Repository: facebook/FBNotifications Branch: master Commit: 672f61c935e8 Files: 195 Total size: 616.0 KB Directory structure: gitextract_ihx0v_r0/ ├── .gitignore ├── .gitmodules ├── .travis.yml ├── Android/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── notifications/ │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── notifications/ │ │ ├── NotificationCardResult.java │ │ ├── NotificationsManager.java │ │ └── internal/ │ │ ├── activity/ │ │ │ └── CardActivity.java │ │ ├── appevents/ │ │ │ └── AppEventsLogger.java │ │ ├── asset/ │ │ │ ├── Asset.java │ │ │ ├── AssetManager.java │ │ │ ├── ParcelableAssetHandler.java │ │ │ ├── cache/ │ │ │ │ ├── CacheOperation.java │ │ │ │ ├── ContentCache.java │ │ │ │ ├── ContentDownloader.java │ │ │ │ └── DiskCache.java │ │ │ └── handlers/ │ │ │ ├── BitmapAssetHandler.java │ │ │ ├── ColorAssetHandler.java │ │ │ └── GifAssetHandler.java │ │ ├── configuration/ │ │ │ ├── ActionConfiguration.java │ │ │ ├── ActionsConfiguration.java │ │ │ ├── BodyConfiguration.java │ │ │ ├── CardConfiguration.java │ │ │ └── HeroConfiguration.java │ │ ├── content/ │ │ │ ├── Content.java │ │ │ ├── ContentManager.java │ │ │ └── TextContent.java │ │ ├── utilities/ │ │ │ ├── EnumCreator.java │ │ │ ├── FontUtilities.java │ │ │ ├── GifDecoder.java │ │ │ ├── InvalidParcelException.java │ │ │ ├── JSONObjectVisitor.java │ │ │ ├── RoundedViewHelper.java │ │ │ ├── TransparentStateListDrawable.java │ │ │ └── Version.java │ │ └── view/ │ │ ├── ActionButton.java │ │ ├── ActionsView.java │ │ ├── AssetView.java │ │ ├── BodyView.java │ │ ├── CardView.java │ │ ├── ContentView.java │ │ ├── GifView.java │ │ └── HeroView.java │ ├── notifications-example/ │ │ ├── build.gradle │ │ ├── google-services.json │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ ├── example1.json │ │ │ ├── example2.json │ │ │ ├── example3.json │ │ │ ├── example4.json │ │ │ └── example5.json │ │ ├── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── notifications/ │ │ │ └── sample/ │ │ │ ├── MainActivity.java │ │ │ ├── MyGcmListenerService.java │ │ │ ├── MyInstanceIDListenerService.java │ │ │ └── RegistrationIntentService.java │ │ └── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ └── content_main.xml │ │ ├── menu/ │ │ │ └── menu_main.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v21/ │ │ │ └── styles.xml │ │ └── values-w820dp/ │ │ └── dimens.xml │ └── settings.gradle ├── CHANGELOG-Android.md ├── CHANGELOG-iOS.md ├── CONTRIBUTING.md ├── FBNotifications.podspec ├── Format/ │ ├── Format-1.0.md │ └── Format-Changelog.md ├── LICENSE ├── LICENSE-specification ├── README.md └── iOS/ ├── .gitignore ├── FBNotifications/ │ ├── Configurations/ │ │ ├── FBNotifications-iOS-Dynamic.xcconfig │ │ └── FBNotifications-iOS.xcconfig │ ├── FBNotifications/ │ │ ├── FBNCardViewController.h │ │ ├── FBNCardViewController.m │ │ ├── FBNConstants.h │ │ ├── FBNConstants.m │ │ ├── FBNotifications.h │ │ ├── FBNotificationsManager.h │ │ ├── FBNotificationsManager.m │ │ ├── Internal/ │ │ │ ├── AppEvents/ │ │ │ │ ├── FBNCardAppEventsLogger.h │ │ │ │ └── FBNCardAppEventsLogger.m │ │ │ ├── Asset/ │ │ │ │ ├── Cache/ │ │ │ │ │ ├── FBNAssetContentCache.h │ │ │ │ │ ├── FBNAssetContentCache.m │ │ │ │ │ ├── FBNAssetContentCacheOperation.h │ │ │ │ │ └── FBNAssetContentCacheOperation.m │ │ │ │ ├── Color/ │ │ │ │ │ ├── FBNColorAsset.h │ │ │ │ │ ├── FBNColorAsset.m │ │ │ │ │ ├── FBNColorAssetController.h │ │ │ │ │ ├── FBNColorAssetController.m │ │ │ │ │ ├── FBNColorAssetViewController.h │ │ │ │ │ └── FBNColorAssetViewController.m │ │ │ │ ├── Common/ │ │ │ │ │ ├── FBNAsset.h │ │ │ │ │ └── FBNAssetController.h │ │ │ │ ├── FBNAssetsController.h │ │ │ │ ├── FBNAssetsController.m │ │ │ │ ├── GIF/ │ │ │ │ │ ├── FBNGIFAsset.h │ │ │ │ │ ├── FBNGIFAsset.m │ │ │ │ │ ├── FBNGIFAssetController.h │ │ │ │ │ ├── FBNGIFAssetController.m │ │ │ │ │ ├── FBNIGIFAssetViewController.h │ │ │ │ │ └── FBNIGIFAssetViewController.m │ │ │ │ └── Image/ │ │ │ │ ├── FBNImageAsset.h │ │ │ │ ├── FBNImageAsset.m │ │ │ │ ├── FBNImageAssetController.h │ │ │ │ ├── FBNImageAssetController.m │ │ │ │ ├── FBNImageAssetViewController.h │ │ │ │ └── FBNImageAssetViewController.m │ │ │ ├── FBNCardViewController_Internal.h │ │ │ ├── Models/ │ │ │ │ ├── Actions/ │ │ │ │ │ ├── FBNCardActionConfiguration.h │ │ │ │ │ ├── FBNCardActionConfiguration.m │ │ │ │ │ ├── FBNCardActionsConfiguration.h │ │ │ │ │ ├── FBNCardActionsConfiguration.m │ │ │ │ │ ├── FBNCardActionsStyle.h │ │ │ │ │ ├── FBNCardActionsStyle.m │ │ │ │ │ └── FBNCardButtonAction.h │ │ │ │ ├── Body/ │ │ │ │ │ ├── FBNCardBodyConfiguration.h │ │ │ │ │ └── FBNCardBodyConfiguration.m │ │ │ │ ├── Card/ │ │ │ │ │ ├── FBNCardConfiguration.h │ │ │ │ │ ├── FBNCardConfiguration.m │ │ │ │ │ ├── FBNCardDisplayOptions.h │ │ │ │ │ ├── FBNCardDisplayOptions.m │ │ │ │ │ ├── FBNCardSize.h │ │ │ │ │ └── FBNCardSize.m │ │ │ │ ├── Components/ │ │ │ │ │ ├── FBNAnimatedImage.h │ │ │ │ │ ├── FBNAnimatedImage.m │ │ │ │ │ ├── FBNCardBackground.h │ │ │ │ │ ├── FBNCardBackground.m │ │ │ │ │ ├── FBNCardColor.h │ │ │ │ │ ├── FBNCardColor.m │ │ │ │ │ ├── FBNCardFont.h │ │ │ │ │ ├── FBNCardFont.m │ │ │ │ │ ├── FBNCardTextAlignment.h │ │ │ │ │ ├── FBNCardTextAlignment.m │ │ │ │ │ ├── FBNCardTextContent.h │ │ │ │ │ └── FBNCardTextContent.m │ │ │ │ └── Hero/ │ │ │ │ ├── FBNCardHeroConfiguration.h │ │ │ │ └── FBNCardHeroConfiguration.m │ │ │ ├── Utilities/ │ │ │ │ ├── FBNCardError.h │ │ │ │ ├── FBNCardError.m │ │ │ │ ├── FBNCardHash.h │ │ │ │ ├── FBNCardHash.m │ │ │ │ ├── FBNCardPayload.h │ │ │ │ ├── FBNCardPayload.m │ │ │ │ ├── FBNCardViewUtilities.h │ │ │ │ ├── FBNCardViewUtilities.m │ │ │ │ └── FBNContentSizeProvider.h │ │ │ └── Views/ │ │ │ ├── Components/ │ │ │ │ ├── FBNCardDismissButton.h │ │ │ │ ├── FBNCardDismissButton.m │ │ │ │ ├── FBNCardLabel.h │ │ │ │ └── FBNCardLabel.m │ │ │ ├── FBNCardActionButton.h │ │ │ ├── FBNCardActionButton.m │ │ │ ├── FBNCardActionsViewController.h │ │ │ ├── FBNCardActionsViewController.m │ │ │ ├── FBNCardBodyViewController.h │ │ │ ├── FBNCardBodyViewController.m │ │ │ ├── FBNCardHeroViewController.h │ │ │ └── FBNCardHeroViewController.m │ │ └── Resources/ │ │ └── Info.plist │ └── FBNotifications.xcodeproj/ │ ├── project.pbxproj │ └── xcshareddata/ │ └── xcschemes/ │ ├── FBNotifications-iOS-Dynamic.xcscheme │ └── FBNotifications-iOS.xcscheme ├── FBNotifications.xcworkspace/ │ └── contents.xcworkspacedata ├── FBNotificationsExample/ │ ├── .gitignore │ ├── FBNotificationsExample/ │ │ ├── Resources/ │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Examples/ │ │ │ │ ├── example1.json │ │ │ │ ├── example2.json │ │ │ │ ├── example3.json │ │ │ │ ├── example4.json │ │ │ │ └── example5.json │ │ │ └── Info.plist │ │ └── Source/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── ViewController.h │ │ ├── ViewController.m │ │ └── main.m │ └── FBNotificationsExample.xcodeproj/ │ ├── project.pbxproj │ └── xcshareddata/ │ └── xcschemes/ │ └── FBNotificationsExample.xcscheme └── Rakefile ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ## OS X .DS_Store ================================================ FILE: .gitmodules ================================================ [submodule "iOS/Vendor/xctoolchain"] path = iOS/Vendor/xctoolchain url = https://github.com/ParsePlatform/xctoolchain.git ================================================ FILE: .travis.yml ================================================ branches: only: - master matrix: include: - language: objective-c os: osx osx_image: xcode7.3 env: TEST_TYPE=iOS - language: objective-c os: osx osx_image: xcode7.3 env: TEST_TYPE=CocoaPods - language: objective-c os: osx osx_image: xcode7.3 env: TEST_TYPE=Carthage - language: android sudo: false env: TEST_TYPE=Android android: components: - tools - build-tools-23.0.3 - android-23 - doc-23 - extra-android-support - extra-android-m2repository - extra-google-m2repository - extra-google-google_play_services install: - | if [ "$TEST_TYPE" = "iOS" ]; then gem install xcpretty -N --no-ri --no-rdoc elif [ "$TEST_TYPE" = Carthage ]; then brew install carthage || brew upgrade carthage fi script: - | if [ "$TEST_TYPE" = "iOS" ]; then cd iOS set -o pipefail xcodebuild build -workspace FBNotifications.xcworkspace -scheme FBNotifications-iOS -sdk iphonesimulator -configuration Release | xcpretty -c xcodebuild build -workspace FBNotifications.xcworkspace -scheme FBNotificationsExample -sdk iphonesimulator -configuration Release | xcpretty -c elif [ "$TEST_TYPE" = Android ]; then cd Android ./gradlew clean assemble -p notifications elif [ "$TEST_TYPE" = CocoaPods ]; then pod lib lint FBNotifications.podspec pod lib lint --use-libraries FBNotifications.podspec elif [ "$TEST_TYPE" = Carthage ]; then carthage build --no-skip-current fi ================================================ FILE: Android/.gitignore ================================================ # built application files *.apk *.ap_ # files for the dex VM *.dex # Java class files *.class # generated files bin/ gen/ # Local configuration file (sdk path, etc) local.properties # Android Studio .idea *.iml *.ipr *.iws classes gen-external-apklibs # Gradle .gradle build # Other .metadata */bin/* */gen/* ================================================ FILE: Android/build.gradle ================================================ buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.1.0' classpath 'com.google.gms:google-services:1.3.0' } } allprojects { repositories { jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: Android/gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Feb 08 14:52:05 PST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip ================================================ FILE: Android/gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true ================================================ FILE: Android/gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: Android/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: Android/notifications/build.gradle ================================================ apply plugin: 'com.android.library' group = 'com.facebook.android' buildscript { repositories { mavenCentral() } } android { compileSdkVersion 23 buildToolsVersion "23.0.3" defaultConfig { minSdkVersion 15 targetSdkVersion 23 versionCode 1 versionName project.version } } configurations { javadoc } dependencies { testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.2.1' javadoc 'com.android.support:appcompat-v7:23.2.1' } apply plugin: 'maven' apply plugin: 'signing' def isSnapshot = version.endsWith('-SNAPSHOT') def ossrhUsername = hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : System.getenv('CI_NEXUS_USERNAME') def ossrhPassword = hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : System.getenv('CI_NEXUS_PASSWORD') task setVersion { // The version will be derived from source project.version = null def sdkVersionFile = file('src/main/java/com/facebook/notifications/NotificationsManager.java') sdkVersionFile.eachLine{ def matcher = (it =~ /(?:.*LIBRARY_VERSION = \")(.*)(?:\".*)/) if (matcher.matches()) { project.version = matcher[0][1] return } } if (project.version.is('unspecified')) { throw new GradleScriptException('Version could not be found.', null) } } uploadArchives { repositories.mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { authentication(userName: ossrhUsername, password: ossrhPassword) } snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { authentication(userName: ossrhUsername, password: ossrhPassword) } pom.project { name 'In-App Notifications Framework' artifactId = 'notifications' packaging 'aar' description 'Facebook In-App Notifications Framework for Android' url 'https://github.com/facebook/FBNotifications' scm { connection 'scm:git@github.com:facebook/FBNotifications.git' developerConnection 'scm:git@github.com:facebook/FBNotifications.git' url 'https://github.com/facebook/FBNotifications' } licenses { license { name 'Facebook Platform License' url 'https://github.com/facebook/FBNotifications/blob/master/LICENSE' distribution 'repo' } } developers { developer { id 'facebook' name 'Facebook' } } } } } uploadArchives.dependsOn(setVersion) signing { required { !isSnapshot && gradle.taskGraph.hasTask("uploadArchives") } sign configurations.archives } task androidJavadocs(type: Javadoc) { source = android.sourceSets.main.java.srcDirs ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar" classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) classpath += configurations.javadoc options.linksOffline("http://d.android.com/reference", "${android.sdkDirectory}/docs/reference") // JDK 1.8 is more strict then 1.7. Have JDK 1.8 behave like 1.7 for javadoc generation if (org.gradle.internal.jvm.Jvm.current().getJavaVersion() == JavaVersion.VERSION_1_8) { options.addStringOption('Xdoclint:none', '-quiet') } exclude '**/internal/**' } task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { classifier = 'javadoc' from androidJavadocs.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.sourceFiles } artifacts { archives androidSourcesJar archives androidJavadocsJar } ================================================ FILE: Android/notifications/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/richardross/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: Android/notifications/src/main/AndroidManifest.xml ================================================ ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/NotificationCardResult.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; public class NotificationCardResult implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public NotificationCardResult createFromParcel(Parcel source) { return new NotificationCardResult(source); } @Override public NotificationCardResult[] newArray(int size) { return new NotificationCardResult[size]; } }; private final @Nullable Uri actionUri; public NotificationCardResult(@Nullable Uri actionUri) { this.actionUri = actionUri; } private NotificationCardResult(Parcel parcel) { this.actionUri = parcel.readParcelable(getClass().getClassLoader()); } @Nullable public Uri getActionUri() { return actionUri; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(actionUri, flags); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/NotificationsManager.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.facebook.notifications.internal.activity.CardActivity; import com.facebook.notifications.internal.appevents.AppEventsLogger; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.asset.handlers.BitmapAssetHandler; import com.facebook.notifications.internal.asset.handlers.ColorAssetHandler; import com.facebook.notifications.internal.asset.handlers.GifAssetHandler; import com.facebook.notifications.internal.configuration.CardConfiguration; import com.facebook.notifications.internal.content.ContentManager; import com.facebook.notifications.internal.utilities.Version; import org.json.JSONException; import org.json.JSONObject; /** * Manages incoming remote notifications for push card presentation. */ public final class NotificationsManager { /** * Represents handlers to be invoked when preparation of a card is completed. */ public interface PrepareCallback { void onPrepared(@NonNull Intent presentationIntent); void onError(@NonNull Exception exception); } /** * Allows for customizing of notifications before they are displayed. */ public interface NotificationExtender { Notification.Builder extendNotification(@NonNull Notification.Builder notification); } /** * The request code to use for intents returned by {@link PrepareCallback} */ public static final int REQUEST_CODE = 0xCA4D; // CARD /** * The intent extra key to be used for pending intents used in notifications created by * `presentNotification()` */ public static final String EXTRA_PAYLOAD_INTENT = "notification_push_payload_intent"; /** * The highest supported payload version by this version of the Notifications SDK */ public static final String PAYLOAD_VERSION = "1.0"; /** * The version of the In-App Notifications Library */ public static final String LIBRARY_VERSION = "1.0.2"; private static final @NonNull Version PAYLOAD_VERSION_OBJECT = new Version(1, 0, 0); private static final String LOG_TAG = NotificationsManager.class.getCanonicalName(); private static final String NOTIFICATION_TAG = "fb_notification_tag"; private static final String PUSH_PAYLOAD_KEY = "fb_push_payload"; private static final String CARD_PAYLOAD_KEY = "fb_push_card"; private static final AssetManager ASSET_MANAGER = new AssetManager(); private static final ContentManager CONTENT_MANAGER = new ContentManager(); static { ASSET_MANAGER.registerHandler(BitmapAssetHandler.TYPE, new BitmapAssetHandler()); ASSET_MANAGER.registerHandler(ColorAssetHandler.TYPE, new ColorAssetHandler()); ASSET_MANAGER.registerHandler(GifAssetHandler.TYPE, new GifAssetHandler()); } private NotificationsManager() { } @Nullable private static JSONObject getPushJSON(@NonNull Bundle bundle) throws JSONException { String pushPayload = bundle.getString(PUSH_PAYLOAD_KEY); if (pushPayload == null) { return null; } return new JSONObject(pushPayload); } @Nullable private static JSONObject getCardJSON(@NonNull Bundle bundle) throws JSONException { String cardPayload = bundle.getString(CARD_PAYLOAD_KEY); if (cardPayload == null) { return null; } return new JSONObject(cardPayload); } @Nullable private static Intent intentForBundle( @NonNull Context context, @Nullable JSONObject pushJSON, @NonNull JSONObject cardJSON, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager ) throws JSONException { Version cardVersion = Version.parse(cardJSON.optString("version")); if (cardVersion == null || cardVersion.compareTo(PAYLOAD_VERSION_OBJECT) > 0) { return null; } Intent intent = new Intent(context, CardActivity.class); if (pushJSON != null) { String campaignIdentifier = AppEventsLogger.getCampaignIdentifier(pushJSON); if (campaignIdentifier != null) { intent.putExtra(CardActivity.EXTRA_CAMPAIGN_IDENTIFIER, campaignIdentifier); } } intent.putExtra(CardActivity.EXTRA_ASSET_MANAGER, assetManager); intent.putExtra(CardActivity.EXTRA_CONTENT_MANAGER, contentManager); intent.putExtra(CardActivity.EXTRA_CARD_PAYLOAD, cardJSON.toString()); return intent; } @NonNull private static AssetManager getAssetManager(@NonNull Context context) { AssetManager manager = new AssetManager(ASSET_MANAGER); manager.setContext(context); return manager; } @NonNull private static ContentManager getContentManager(@NonNull Context context) { ContentManager manager = new ContentManager(CONTENT_MANAGER); manager.setContext(context); return manager; } /** * Returns whether or not a notification bundle has a valid push payload. * * @param notificationBundle The bundle to check for a push payload */ public static boolean canPresentCard(@NonNull Bundle notificationBundle) { return notificationBundle.containsKey(CARD_PAYLOAD_KEY); } /** * Present an intent from a given activity to show the notification bundle contained in notificationBundle. * * @param activity The activity to present from * @param notificationBundle The bundle containing the notification payload to present * @return whether or not the activity could successfully be presented */ public static boolean presentCard(@NonNull Activity activity, @NonNull Bundle notificationBundle) { try { JSONObject cardJSON = getCardJSON(notificationBundle); if (cardJSON == null) { return false; } JSONObject pushJSON = getPushJSON(notificationBundle); Intent presentationIntent = intentForBundle(activity, pushJSON, cardJSON, getAssetManager(activity), getContentManager(activity) ); if (presentationIntent == null) { return false; } activity.startActivityForResult(presentationIntent, REQUEST_CODE); return true; } catch (JSONException ex) { Log.e(LOG_TAG, "Error while parsing JSON", ex); return false; } } /** * Prepare and pre-load a notification bundle into memory. * * @param context The current context of your program. Usually an activity, application, or * service context. * @param notificationBundle The bundle containing the notification payload to present * @param callback The callback to invoke once preparation is complete. This is guaranteed to be * invoked on the same thread as this method is invoked from. */ public static void prepareCard( @NonNull final Context context, @NonNull final Bundle notificationBundle, @NonNull final PrepareCallback callback ) { final Handler handler = new Handler(); final AssetManager assetManager = getAssetManager(context); final ContentManager contentManager = getContentManager(context); // Cache and prepare in background. new Thread() { @Override public void run() { try { JSONObject cardJSON = getCardJSON(notificationBundle); if (cardJSON == null) { throw new NullPointerException("No content present in the notification bundle."); } Version cardVersion = Version.parse(cardJSON.optString("version")); if (cardVersion == null || cardVersion.compareTo(PAYLOAD_VERSION_OBJECT) > 0) { throw new Exception("Payload version " + cardVersion + " not supported by this version of the notifications SDK."); } assetManager.cachePayload(cardJSON, new AssetManager.CacheCompletionCallback() { @Override public void onCacheCompleted(@NonNull JSONObject payload) { assetManager.stopCaching(); try { JSONObject cardJSON = getCardJSON(notificationBundle); final Intent presentIntent = intentForBundle(context, getPushJSON(notificationBundle), cardJSON, assetManager, contentManager); if (presentIntent == null) { throw new NullPointerException("presentIntent was null, this should never happen!"); } CardConfiguration configuration = new CardConfiguration(cardJSON, assetManager, contentManager); presentIntent.putExtra(CardActivity.EXTRA_CONFIGURATION, configuration); handler.post(new Runnable() { @Override public void run() { callback.onPrepared(presentIntent); } }); } catch (final Exception ex) { handler.post(new Runnable() { @Override public void run() { callback.onError(ex); } }); } } }); } catch (final Exception ex) { handler.post(new Runnable() { @Override public void run() { callback.onError(ex); } }); } } }.start(); } /** * Handle the result of an activity started using * {@code prepare(Context, Bundle, PrepareCallback)} or {@code present(Activity, Bundle)}. * * @param requestCode The request code used to start the activity * @param resultCode The result code returned by the activity * @param data The data returned by the activity * @return The notification card result of the activity if it exists, or null if it does not. */ public static NotificationCardResult handleActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode != REQUEST_CODE) { return null; } if (resultCode != Activity.RESULT_OK || data == null) { return null; } return data.getParcelableExtra(CardActivity.EXTRA_NOTIFICATION_CARD_RESULT); } /** * Present a {@link Notification} to be presented from a GCM push bundle. *

* This does not present a notification immediately, instead it caches the assets from the * notification bundle, and then presents a notification to the user. This allows for a smoother * interaction without loading indicators for the user. *

* Note that only one notification can be created for a specific push bundle, should you attempt * to present a new notification with the same payload bundle as an existing notification, it will * replace and update the old notification. * * @param context The context to send the notification from * @param notificationBundle The content of the push notification * @param launcherIntent The launcher intent that contains your Application's activity. * This will be modified with the FLAG_ACTIVITY_CLEAR_TOP and * FLAG_ACTIVITY_SINGLE_TOP flags, in order to properly show the * notification in an already running application. *

* Should you not want this behavior, you may use the notificationExtender * parameter to customize the contentIntent of the notification before * presenting it. */ public static boolean presentNotification( @NonNull Context context, @NonNull Bundle notificationBundle, @NonNull Intent launcherIntent) { return presentNotification(context, notificationBundle, launcherIntent, null); } /** * Present a {@link Notification} to be presented from a GCM push bundle. *

* This does not present a notification immediately, instead it caches the assets from the * notification bundle, and then presents a notification to the user. This allows for a smoother * interaction without loading indicators for the user. *

* Note that only one notification can be created for a specific push bundle, should you attempt * to present a new notification with the same payload bundle as an existing notification, it will * replace and update the old notification. * * @param context The context to send the notification from * @param notificationBundle The content of the push notification * @param launcherIntent The launcher intent that contains your Application's activity. * This will be modified with the FLAG_ACTIVITY_CLEAR_TOP and * FLAG_ACTIVITY_SINGLE_TOP flags, in order to properly show the * notification in an already running application. *

* Should you not want this behavior, you may use the notificationExtender * parameter to customize the contentIntent of the notification before * presenting it. * @param notificationExtender A nullable argument that allows you to customize the notification * before displaying it. Use this to configure Icons, text, sounds, * etc. before we pass the notification off to the OS. */ public static boolean presentNotification( @NonNull final Context context, @NonNull final Bundle notificationBundle, @NonNull final Intent launcherIntent, @Nullable final NotificationExtender notificationExtender) { final JSONObject alert; final int payloadHash; try { String payload = notificationBundle.getString(CARD_PAYLOAD_KEY); if (payload == null) { return false; } payloadHash = payload.hashCode(); JSONObject payloadObject = new JSONObject(payload); alert = payloadObject.optJSONObject("alert") != null ? payloadObject.optJSONObject("alert") : new JSONObject(); } catch (JSONException ex) { Log.e(LOG_TAG, "Error while parsing notification bundle JSON", ex); return false; } final boolean[] success = new boolean[1]; final Thread backgroundThread = new Thread(new Runnable() { @Override public void run() { Looper.prepare(); prepareCard(context, notificationBundle, new PrepareCallback() { @Override public void onPrepared(@NonNull Intent presentationIntent) { Intent contentIntent = new Intent(launcherIntent); contentIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); contentIntent.putExtra(EXTRA_PAYLOAD_INTENT, presentationIntent); NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); Notification.Builder builder = new Notification.Builder(context) .setSmallIcon(android.R.drawable.ic_dialog_alert) .setContentTitle(alert.optString("title")) .setContentText(alert.optString("body")) .setAutoCancel(true) .setContentIntent( PendingIntent.getActivity( context.getApplicationContext(), payloadHash, contentIntent, PendingIntent.FLAG_ONE_SHOT ) ); if (notificationExtender != null) { builder = notificationExtender.extendNotification(builder); } manager.notify(NOTIFICATION_TAG, payloadHash, builder.getNotification()); success[0] = true; Looper.myLooper().quit(); } @Override public void onError(@NonNull Exception exception) { Log.e(LOG_TAG, "Error while preparing card", exception); Looper.myLooper().quit(); } }); Looper.loop(); } }); backgroundThread.start(); try { backgroundThread.join(); } catch (InterruptedException ex) { Log.e(LOG_TAG, "Failed to wait for background thread", ex); return false; } return success[0]; } /** * Present a card from the notification this activity * was created from, if the notification exists. * * @param activity The activity to present from. * @return Whether or not a card was presented. */ public static boolean presentCardFromNotification(@NonNull Activity activity) { return presentCardFromNotification(activity, activity.getIntent()); } /** * Present a card from the notification this activity * was relaunched from, if the notification exists. * * @param activity The activity to present from. * @param intent Intent that was used to re-launch the activity. * @return Whether or not a card was presented. */ public static boolean presentCardFromNotification(@NonNull Activity activity, @NonNull Intent intent) { Intent notificationIntent = intent.getParcelableExtra(EXTRA_PAYLOAD_INTENT); if (notificationIntent == null) { return false; } activity.startActivityForResult(notificationIntent, REQUEST_CODE); return true; } /** * Registers an Asset Handler for use. * * @param assetType The type of the asset to register for. * @param assetHandler The asset handler to register. */ private static void registerAssetHandler( @NonNull String assetType, @NonNull AssetManager.AssetHandler assetHandler ) { ASSET_MANAGER.registerHandler(assetType, assetHandler); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/activity/CardActivity.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.activity; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import android.view.Gravity; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; import android.widget.ProgressBar; import com.facebook.notifications.NotificationCardResult; import com.facebook.notifications.internal.appevents.AppEventsLogger; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.asset.handlers.ColorAssetHandler; import com.facebook.notifications.internal.configuration.CardConfiguration; import com.facebook.notifications.internal.content.ContentManager; import com.facebook.notifications.internal.view.ActionButton; import com.facebook.notifications.internal.view.ActionsView; import com.facebook.notifications.internal.view.CardView; import org.json.JSONException; import org.json.JSONObject; /** * An activity which displays a push card. */ public class CardActivity extends Activity implements ActionsView.Delegate { /** * The intent extra key to be set with the JSON payload of your push card payload. */ public static final String EXTRA_CARD_PAYLOAD = "fb_push_card_payload"; /** * If the configuration has already been cached and parsed, you may pass simply the entire * configuration object to the activity. This is significantly faster than re-parsing the entire * JSON object. */ public static final String EXTRA_CONFIGURATION = "fb_push_card_configuration"; /** * The intent extra key to be set with the push campaign identifier. */ public static final String EXTRA_CAMPAIGN_IDENTIFIER = "fb_push_campaign"; /** * The intent extra key to be set with an instance of {@link AssetManager} */ public static final String EXTRA_ASSET_MANAGER = "fb_push_card_asset_manager"; /** * The intent extra key to be set with an instance of {@link ContentManager} */ public static final String EXTRA_CONTENT_MANAGER = "fb_push_card_content_manager"; /** * The intent extra key to be set with the card result. */ public static final String EXTRA_NOTIFICATION_CARD_RESULT = "fb_notification_card_result"; private static final String LOG_TAG = CardActivity.class.getCanonicalName(); private @Nullable String campaignIdentifier; private @Nullable JSONObject configurationPayload; private @NonNull AssetManager assetManager; private @NonNull ContentManager contentManager; private @NonNull AppEventsLogger appEventsLogger; private @Nullable CardView cardView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); appEventsLogger = new AppEventsLogger(this); Intent intent = getIntent(); campaignIdentifier = intent.getStringExtra(EXTRA_CAMPAIGN_IDENTIFIER); String payloadString = intent.getStringExtra(EXTRA_CARD_PAYLOAD); CardConfiguration configuration = intent.getParcelableExtra(EXTRA_CONFIGURATION); AssetManager assetManager = intent.getParcelableExtra(EXTRA_ASSET_MANAGER); ContentManager contentManager = intent.getParcelableExtra(EXTRA_CONTENT_MANAGER); assetManager = assetManager != null ? assetManager : new AssetManager(); contentManager = contentManager != null ? contentManager : new ContentManager(); assetManager.setContext(this); contentManager.setContext(this); this.assetManager = assetManager; this.contentManager = contentManager; try { configurationPayload = new JSONObject(payloadString); } catch (JSONException ex) { Log.e(LOG_TAG, "Error parsing JSON payload", ex); } if (configuration == null) { beginLoadingContent(); } else { displayConfiguration(configuration); } appEventsLogger.logPushOpen(campaignIdentifier); } @Override protected void onDestroy() { super.onDestroy(); if (assetManager == null) { return; } assetManager.stopCaching(); if (configurationPayload == null) { return; } assetManager.clearCache(configurationPayload); } private void beginLoadingContent() { if (assetManager == null || contentManager == null) { Log.e(LOG_TAG, "Asset & content manager should be available!"); return; } if (configurationPayload == null) { Log.e(LOG_TAG, "No card payload is available!"); return; } ProgressBar loadingView = new ProgressBar(this); loadingView.setIndeterminate(true); int backgroundColor = ColorAssetHandler.fromRGBAHex(configurationPayload.optString("backdropColor")); FrameLayout loadingViewFrame = new FrameLayout(this); loadingViewFrame.setBackgroundColor(backgroundColor); LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); layoutParams.gravity = Gravity.CENTER; loadingViewFrame.addView(loadingView, layoutParams); setContentView(loadingViewFrame); final Handler handler = new Handler(); if (configurationPayload == null) { return; } assetManager.cachePayload(configurationPayload, new AssetManager.CacheCompletionCallback() { @Override public void onCacheCompleted(@NonNull JSONObject payload) { final CardConfiguration configuration; try { configuration = new CardConfiguration(configurationPayload, assetManager, contentManager); } catch (JSONException ex) { Log.e(LOG_TAG, "Error while parsing JSON", ex); return; } handler.post(new Runnable() { @Override public void run() { displayConfiguration(configuration); } }); } }); } private void displayConfiguration(CardConfiguration configuration) { if (assetManager == null || contentManager == null) { Log.e(LOG_TAG, "Asset or content manager has unloaded since beginLoadingContent()!"); return; } cardView = new CardView(this, assetManager, contentManager, this, configuration); setContentView(cardView); } @Override public void onBackPressed() { appEventsLogger.logButtonAction(ActionButton.Type.Dismiss, campaignIdentifier); Intent resultIntent = new Intent(); resultIntent.putExtra(EXTRA_NOTIFICATION_CARD_RESULT, new NotificationCardResult(null)); setResult(RESULT_OK, resultIntent); finish(); } @Override public void actionButtonClicked(ActionButton.Type type, @Nullable Uri actionUri) { appEventsLogger.logButtonAction(type, campaignIdentifier); Intent resultIntent = new Intent(); resultIntent.putExtra(EXTRA_NOTIFICATION_CARD_RESULT, new NotificationCardResult(actionUri)); setResult(RESULT_OK, resultIntent); finish(); if (actionUri == null) { return; } Intent actionIntent = new Intent(Intent.ACTION_VIEW, actionUri); startActivity(actionIntent); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/appevents/AppEventsLogger.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.appevents; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.facebook.notifications.internal.view.ActionButton; import org.json.JSONObject; import java.lang.reflect.Method; /** * Simple class to call into core Facebook SDK and create/invoke the logger from there to report * App Events that have been triggered by a push card. */ public class AppEventsLogger { private static final String LOG_TAG = AppEventsLogger.class.getCanonicalName(); private static final String PUSH_OPEN_EVENT = "fb_mobile_push_opened"; private static final String PUSH_CAMPAIGN_KEY = "fb_push_campaign"; private @Nullable Object fbSDKLogger; private @Nullable Method fbSDKLogMethod; public AppEventsLogger(@NonNull Context context) { try { Class loggerClass = Class.forName("com.facebook.appevents.AppEventsLogger"); Method instantiateMethod = loggerClass.getMethod("newLogger", Context.class); fbSDKLogMethod = loggerClass.getMethod("logEvent", String.class, Bundle.class); fbSDKLogger = instantiateMethod.invoke(null, context); } catch (Exception ex) { Log.w( LOG_TAG, "Failed to initialize AppEventsLogger. " + "Did you forget to include the Facebook SDK in your application?", ex ); } } @Nullable public static String getCampaignIdentifier(@NonNull JSONObject payload) { return payload.optString("campaign", null); } @NonNull private static String getAppEventName(ActionButton.Type action) { switch (action) { case Primary: return "fb_mobile_push_card_action_primary"; case Secondary: return "fb_mobile_push_card_action_secondary"; case Dismiss: return "fb_mobile_push_card_action_dismiss"; default: throw new RuntimeException("Unknown action type: " + action); } } public void logPushOpen(@Nullable String campaignIdentifier) { logEvent(PUSH_OPEN_EVENT, campaignIdentifier); } public void logButtonAction(ActionButton.Type action, @Nullable String campaignIdentifier) { logEvent(getAppEventName(action), campaignIdentifier); } private void logEvent(@NonNull String eventName, @Nullable String campaignIdentifier) { if (campaignIdentifier == null || campaignIdentifier.equals("") || fbSDKLogger == null || fbSDKLogMethod == null) { return; } Bundle parameters = new Bundle(); parameters.putString(PUSH_CAMPAIGN_KEY, campaignIdentifier); try { fbSDKLogMethod.invoke(fbSDKLogger, eventName, parameters); } catch (Exception ex) { Log.w( LOG_TAG, "Failed to invoke AppEventsLogger." + "Did you forget to include the Facebook SDK in your application?", ex ); } } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/Asset.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset; import android.os.Parcelable; import android.support.annotation.NonNull; import com.facebook.notifications.internal.utilities.InvalidParcelException; /** * A 'marker' interface used for identifying an object as a resource. On its own, doesn't do much. */ public interface Asset extends Parcelable { /** * Get the type of this Asset. Used by {@link AssetManager} to find the proper * {@link AssetManager.AssetHandler} for this asset. * * @return The asset type of the asset. */ @NonNull String getType(); /** * Validate that all of the content for this asset has been properly cached and prepared. * * @throws InvalidParcelException */ void validate() throws InvalidParcelException; } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/AssetManager.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset; import android.content.Context; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import com.facebook.notifications.internal.asset.cache.ContentCache; import com.facebook.notifications.internal.utilities.InvalidParcelException; import com.facebook.notifications.internal.utilities.JSONObjectVisitor; import org.json.JSONObject; import java.io.File; import java.net.URL; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * Creates different Asset types based on the contents of a JSON payload */ public class AssetManager implements Parcelable { /** * Allows for the fetching of previously-fetched content from an on-disk cache. */ public interface AssetCache { /** * Gets the on-disk file for a previously cached URL. * * @param url The url to fetch from the cache * @return The file which contains the contents of the url, or null if the URL was not * previously cached, or if caching failed */ File getCachedFile(URL url); } /** * An interface that allows different types of {@link Asset} to be displayed and used by the push * card. * * This must be either {@link Parcelable} or constructable with a default constructor. */ public interface AssetHandler { /** * Invoked by {@link AssetManager} when an asset is being cached * * @param payload The payload to cache * @return A set of URLs that contains the content to download into the asset cache, or null if * no URLs exist to cache. */ @Nullable Set getCacheURLs(@NonNull JSONObject payload); /** * Invoked by {@link AssetManager} when an asset should be inflated from a JSON payload * All of the URLs returned by `getCacheURLs()` are guaranteed to have been fully downloaded * * @param payload The payload to inflate from * @param cache The cache which contains the URLs that have been requested to be downloaded * @return The fully inflated asset, or null if inflation fails */ @Nullable AssetType createAsset(@NonNull JSONObject payload, @NonNull AssetCache cache); /** * Invoked by {@link AssetManager} when an asset from this handler has been requested to be * displayed * * @param asset The asset to create a view for * @param context The context to create a view for * @return A new view */ @NonNull View createView(@NonNull AssetType asset, @NonNull Context context); } /** * An interface for receiving a callback when the caching of a set of resources has completed. */ public interface CacheCompletionCallback { /** * Invoked by the {@link AssetManager} whenever caching has finished. * * @param payload The payload which has been cached. */ void onCacheCompleted(@NonNull JSONObject payload); } public static final Creator CREATOR = new Creator() { @Override public AssetManager createFromParcel(Parcel source) { try { AssetManager assetManager = new AssetManager(source); assetManager.validate(); return assetManager; } catch (InvalidParcelException ex) { Log.w(LOG_TAG, "Failed to decode asset manager", ex); return null; } } @Override public AssetManager[] newArray(int size) { return new AssetManager[size]; } }; private static final String LOG_TAG = AssetManager.class.getCanonicalName(); private @Nullable ContentCache contentCache; private final @NonNull Map registeredHandlers; public AssetManager() { registeredHandlers = new ConcurrentHashMap<>(); } /** * Creates a new AssetManager from another manager's handlers, but without a context set. * @param other The asset manager to clone */ public AssetManager(AssetManager other) { registeredHandlers = new ConcurrentHashMap<>(other.registeredHandlers); } public AssetManager(@NonNull Parcel parcel) { registeredHandlers = new ConcurrentHashMap<>(); Bundle handlersBundle = parcel.readBundle(getClass().getClassLoader()); for (String type : handlersBundle.keySet()) { registeredHandlers.put(type, (ParcelableAssetHandler)handlersBundle.getParcelable(type)); } } public void setContext(@NonNull Context context) { if (contentCache != null) { throw new UnsupportedOperationException("Can only call setContext() once on an AssetManager!"); } contentCache = new ContentCache(context); } private void validate() throws InvalidParcelException { for (ParcelableAssetHandler handler : registeredHandlers.values()) { handler.validate(); } } public void registerHandler(@NonNull String assetType, @NonNull AssetHandler handler) { registeredHandlers.put(assetType, new ParcelableAssetHandler(handler)); } @Nullable public Asset inflateAsset(@Nullable JSONObject payload) { if (contentCache == null) { throw new UnsupportedOperationException("Cannot call inflateAsset() before setContext() has been called!"); } if (payload == null) { return null; } String type = payload.optString("_type"); AssetHandler handler = registeredHandlers.get(type); if (handler == null) { return null; } return handler.createAsset(payload, contentCache); } @NonNull public View inflateView(@NonNull AssetType asset, @NonNull Context context) { String type = asset.getType(); @SuppressWarnings("unchecked") AssetHandler handler = (AssetHandler) registeredHandlers.get(type); if (handler == null) { throw new IllegalArgumentException("Asset type \"" + type + "\" not registered!"); } return handler.createView(asset, context); } /** * Caches a given JSON payload in the background. * * @param payload The payload to cache * @param callback The callback to be invoked when caching completes. */ public void cachePayload(final @NonNull JSONObject payload, final @NonNull CacheCompletionCallback callback) { if (contentCache == null) { throw new UnsupportedOperationException("Cannot call cachePayload() before setContext() has been called!"); } contentCache.cache(getCacheURLs(payload), new ContentCache.CompletionCallback() { @Override public void onCacheCompleted(@NonNull Set urlsToCache) { callback.onCacheCompleted(payload); } }); } /** * Clears the cache for a given JSON payload. * * @param payload The payload to clear the cache for. */ public void clearCache(@NonNull JSONObject payload) { if (contentCache == null) { throw new UnsupportedOperationException("Cannot call cachePayload() before setContext() has been called!"); } contentCache.clear(getCacheURLs(payload)); } /** * Stops any caching that may be occurring in the background. */ public void stopCaching() { if (contentCache == null) { throw new UnsupportedOperationException("Cannot call stopCaching() before setContext() has been called!"); } contentCache.stop(); } @NonNull private Set getCacheURLs(@Nullable JSONObject payload) { if (contentCache == null) { throw new UnsupportedOperationException("Cannot call stopCaching() before setContext() has been called!"); } if (payload == null) { return new HashSet<>(); } final Set cacheURLs = new HashSet<>(); JSONObjectVisitor.walk(payload, new JSONObjectVisitor() { @Override protected void visit(JSONObject object) { super.visit(object); String type = object.optString("_type"); AssetHandler handler = registeredHandlers.get(type); if (handler == null) { return; } Set newURLs = handler.getCacheURLs(object); if (newURLs == null) { return; } cacheURLs.addAll(newURLs); } }); return cacheURLs; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { Bundle bundle = new Bundle(); for (Map.Entry entry : registeredHandlers.entrySet()) { bundle.putParcelable(entry.getKey(), entry.getValue()); } dest.writeBundle(bundle); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/ParcelableAssetHandler.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.View; import com.facebook.notifications.internal.utilities.InvalidParcelException; import org.json.JSONObject; import java.net.URL; import java.util.Set; /** * Represents a parcelable entry in {@link AssetManager}'s handler list. */ class ParcelableAssetHandler implements AssetManager.AssetHandler, Parcelable { public static final Creator CREATOR = new Creator() { @Override public ParcelableAssetHandler createFromParcel(Parcel source) { return new ParcelableAssetHandler(source); } @Override public ParcelableAssetHandler[] newArray(int size) { return new ParcelableAssetHandler[size]; } }; private final @Nullable AssetManager.AssetHandler handler; private final @Nullable InvalidParcelException exception; @SuppressWarnings("unchecked") public ParcelableAssetHandler(@NonNull AssetManager.AssetHandler handler) { this.handler = (AssetManager.AssetHandler) handler; this.exception = null; } @SuppressWarnings("unchecked") public ParcelableAssetHandler(Parcel source) { boolean isParcelable = source.readInt() != 0; if (isParcelable) { handler = source.readParcelable(getClass().getClassLoader()); exception = null; } else { AssetManager.AssetHandler handler = null; InvalidParcelException exception = null; try { Class assetHandlerClass = (Class) Class.forName(source.readString(), true, getClass().getClassLoader()); handler = (AssetManager.AssetHandler) assetHandlerClass.newInstance(); } catch (Exception ex) { exception = new InvalidParcelException(ex); } this.handler = handler; this.exception = exception; } } public void validate() throws InvalidParcelException { if (exception != null) { throw exception; } if (handler == null) { throw new IllegalStateException("AssetHandler should not be null when parceling if no exception was thrown!"); } } @Nullable @Override public Set getCacheURLs(@NonNull JSONObject payload) { if (handler == null) { throw new IllegalStateException("AssetHandler should not be null, did you forget to call validate()?"); } return handler.getCacheURLs(payload); } @Nullable @Override public Asset createAsset(@NonNull JSONObject payload, @NonNull AssetManager.AssetCache cache) { if (handler == null) { throw new IllegalStateException("AssetHandler should not be null, did you forget to call validate()?"); } return handler.createAsset(payload, cache); } @NonNull @Override public View createView(@NonNull Asset asset, @NonNull Context context) { if (handler == null) { throw new IllegalStateException("AssetHandler should not be null, did you forget to call validate()?"); } return handler.createView(asset, context); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { if (handler == null) { throw new IllegalStateException("AssetHandler should not be null, did you forget to call validate()?"); } if (handler instanceof Parcelable) { dest.writeInt(1); dest.writeParcelable((Parcelable)handler, flags); } else { dest.writeInt(0); dest.writeString(handler.getClass().getName()); } } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/cache/CacheOperation.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset.cache; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.io.File; import java.net.URL; import java.util.HashSet; import java.util.Set; class CacheOperation implements ContentDownloader.DownloadCallback { private final @NonNull Object mutex; private final @NonNull Set urlsToCache; private final @NonNull Set remainingURLs; private final @NonNull ContentCache.CompletionCallback completion; public CacheOperation(@NonNull Set urlsToCache, @NonNull ContentCache.CompletionCallback completion) { this.mutex = new Object(); this.urlsToCache = urlsToCache; this.remainingURLs = new HashSet<>(urlsToCache); this.completion = completion; } @NonNull public Set getUrlsToCache() { return urlsToCache; } @NonNull public ContentCache.CompletionCallback getCompletion() { return completion; } @Override public void onResourceDownloaded(@NonNull URL url, @Nullable File targetFile) { boolean invoke; synchronized (mutex) { remainingURLs.remove(url); invoke = remainingURLs.size() == 0; } if (invoke) { completion.onCacheCompleted(urlsToCache); } } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/cache/ContentCache.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset.cache; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import java.io.File; import java.net.URL; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * Manages the caching of an {@link Asset}'s resources * from the internet */ public class ContentCache implements AssetManager.AssetCache { public interface CompletionCallback { void onCacheCompleted(@NonNull Set urlsToCache); } private static final String LOG_TAG = ContentCache.class.getCanonicalName(); private final @NonNull Context context; private final @NonNull ContentDownloader downloader; private final @NonNull Thread downloadThread; private final @NonNull DiskCache diskCache; private final @NonNull Object synchronizationMutex; private final @NonNull Map> cacheOperations; private @Nullable Set cachedKeys; public ContentCache(@NonNull Context context) { this.context = context; downloader = new ContentDownloader(); diskCache = new DiskCache(context); downloadThread = new Thread(downloader); synchronizationMutex = new Object(); cacheOperations = new HashMap<>(); downloadThread.start(); } @NonNull private static String getCacheKey(@NonNull URL url) { String urlString = url.toString(); try { MessageDigest MD5 = MessageDigest.getInstance("MD5"); Charset UTF8 = Charset.forName("UTF-8"); byte[] toDigest = urlString.getBytes(UTF8); byte[] digested = MD5.digest(toDigest); return String.format( "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", digested[0], digested[1], digested[2], digested[3], digested[4], digested[5], digested[6], digested[7], digested[8], digested[9], digested[10], digested[11], digested[12], digested[13], digested[14], digested[15]); } catch (NoSuchAlgorithmException ex) { // If for some ungodly reason MD5 doesn't exist in this JVM, use the string's built-in hash // code. return Integer.toHexString(urlString.hashCode()); } } public void stop() { downloadThread.interrupt(); try { downloadThread.join(); } catch (InterruptedException ex) { // If we were interrupted while waiting to have our children die, re-interrupt the // child thread and hope for the best. downloadThread.interrupt(); } } @NonNull public Context getContext() { return context; } public void cache(@NonNull Set urlsToCache, @NonNull CompletionCallback completion) { CacheOperation operation = new CacheOperation(urlsToCache, completion); synchronized (synchronizationMutex) { int scheduledCount = 0; for (Iterator urlIterator = operation.getUrlsToCache().iterator(); urlIterator.hasNext(); ) { URL url = urlIterator.next(); final String hashKey = getCacheKey(url); if (hasCachedData(hashKey)) { urlIterator.remove(); continue; } if (cacheOperations.containsKey(hashKey)) { Set existingOperations = cacheOperations.get(hashKey); existingOperations.add(operation); continue; } final Set newOperations = new HashSet<>(); newOperations.add(operation); cacheOperations.put(hashKey, newOperations); downloader.downloadAsync(url, diskCache.fetch(getCacheKey(url)), new ContentDownloader.DownloadCallback() { @Override public void onResourceDownloaded(@NonNull URL url, @Nullable File file) { Set operations; synchronized (synchronizationMutex) { operations = new HashSet<>(newOperations); } for (CacheOperation operation : operations) { operation.onResourceDownloaded(url, file); } } }); scheduledCount++; } if (scheduledCount == 0) { completion.onCacheCompleted(urlsToCache); } } } public void clear(@NonNull Set urlsToClear) { synchronized (synchronizationMutex) { for (URL url : urlsToClear) { String cacheKey = getCacheKey(url); diskCache.remove(cacheKey); if (cachedKeys != null) { cachedKeys.remove(cacheKey); } } } } @Nullable @Override public File getCachedFile(@NonNull URL contentURL) { File file = diskCache.fetch(getCacheKey(contentURL)); if (!file.exists()) { return null; } return file; } private boolean hasCachedData(@NonNull String cacheKey) { synchronized (synchronizationMutex) { if (cachedKeys == null) { cachedKeys = diskCache.getCacheKeys(); } return cachedKeys.contains(cacheKey); } } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/cache/ContentDownloader.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset.cache; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; class ContentDownloader implements Runnable { public interface DownloadCallback { void onResourceDownloaded(@NonNull URL url, @Nullable File file); } private interface DownloadOperation extends Runnable { } private static final String LOG_TAG = ContentDownloader.class.getCanonicalName(); private final @NonNull BlockingQueue operations; public ContentDownloader() { operations = new LinkedBlockingQueue<>(); } @Override public void run() { while (true) { try { DownloadOperation operation = operations.take(); operation.run(); } catch (InterruptedException ex) { // This means our parent thread requested us killed, RIP. return; } } } public void downloadAsync(final @NonNull URL url, final @NonNull File targetFile, final @NonNull DownloadCallback callback) { operations.offer(new DownloadOperation() { private File download(@NonNull URL url) { HttpURLConnection connection = null; InputStream inputStream = null; OutputStream outputStream = null; try { connection = (HttpURLConnection) url.openConnection(); if (connection.getResponseCode() != 200) { return null; } inputStream = connection.getInputStream(); outputStream = new FileOutputStream(targetFile); byte[] buffer = new byte[4096]; while (true) { int read = inputStream.read(buffer); if (read == -1) { break; } outputStream.write(buffer, 0, read); } return targetFile; } catch (Exception ex) { Log.e(LOG_TAG, "Failed to download content for url " + url, ex); return null; } finally { try { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } catch (IOException ex) { Log.e(LOG_TAG, "Failed to close streams", ex); } if (connection != null) { connection.disconnect(); } } } @Override public void run() { callback.onResourceDownloaded(url, download(url)); } }); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/cache/DiskCache.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset.cache; import android.content.Context; import android.support.annotation.NonNull; import android.util.Log; import java.io.File; import java.util.Collections; import java.util.HashSet; import java.util.Set; class DiskCache { private static final String LOG_TAG = DiskCache.class.getCanonicalName(); private final @NonNull File cacheDirectory; public DiskCache(@NonNull Context context) { cacheDirectory = context.getCacheDir(); } public void remove(final @NonNull String key) { File diskFile = new File(cacheDirectory, key); if (!diskFile.delete()) { Log.w(LOG_TAG, "Failed to delete cache file \"" + diskFile.getAbsolutePath() + "\""); } } @NonNull public File fetch(@NonNull String key) { return new File(cacheDirectory, key); } @NonNull public Set getCacheKeys() { String[] files = cacheDirectory.list(); Set keys = new HashSet<>(files.length); Collections.addAll(keys, files); return keys; } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/handlers/BitmapAssetHandler.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset.handlers; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Parcel; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import android.widget.ImageView; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.utilities.InvalidParcelException; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.HashSet; import java.util.Set; /** * Handles assets of the bitmap type */ public class BitmapAssetHandler implements AssetManager.AssetHandler { /** * A resource implementation for Bitmaps read from disk */ static class BitmapAsset implements Asset { public static final Creator CREATOR = new Creator() { @Override public BitmapAsset createFromParcel(Parcel source) { return new BitmapAsset(source); } @Override public BitmapAsset[] newArray(int size) { return new BitmapAsset[size]; } }; private final @NonNull File createdFrom; private transient @Nullable Bitmap bitmap; private BitmapAsset(@NonNull File createdFrom) { this.createdFrom = createdFrom; } private BitmapAsset(@NonNull Parcel parcel) { createdFrom = new File(parcel.readString()); } @Nullable private static Bitmap decodeBitmap(@NonNull File file) { try { // NOTE: We must be careful when decoding images on android. If a malicious push sends down // a payload image that is too large for us to reasonably decode, we must ensure that we can // safely fall back to a lower resolution if we don't have the memory for it. BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 1; while (true) { InputStream cachedData = new FileInputStream(file); try { return BitmapFactory.decodeStream(cachedData, null, options); } catch (OutOfMemoryError ex) { // Ignore out of memory, try loading with less requested resolution. System.gc(); options.inSampleSize *= 2; } finally { cachedData.close(); } } } catch (IOException ex) { Log.e(LOG_TAG, "IO Exception!", ex); return null; } } @NonNull public File getCreatedFrom() { return createdFrom; } @NonNull public Bitmap getBitmap() { if (bitmap == null) { bitmap = decodeBitmap(createdFrom); if (bitmap == null) { throw new RuntimeException("Failed to decode bitmap from file"); } } return bitmap; } @NonNull @Override public String getType() { return TYPE; } @Override public void validate() throws InvalidParcelException { if (!createdFrom.exists()) { throw new InvalidParcelException( new FileNotFoundException( "Bitmap cache file does not exist: " + createdFrom.getAbsolutePath() ) ); } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(createdFrom.getAbsolutePath()); } } public static final String TYPE = "Image"; private static final String LOG_TAG = BitmapAssetHandler.class.getCanonicalName(); @Nullable @Override public Set getCacheURLs(@NonNull JSONObject payload) { try { URL url = new URL(payload.getString("url")); Set set = new HashSet<>(); set.add(url); return set; } catch (MalformedURLException ex) { return null; } catch (JSONException ex) { return null; } } @Nullable @Override public BitmapAsset createAsset(@NonNull JSONObject payload, @NonNull AssetManager.AssetCache cache) { try { URL url = new URL(payload.getString("url")); File cacheFile = cache.getCachedFile(url); if (cacheFile == null) { return null; } return new BitmapAsset(cacheFile); } catch (MalformedURLException ex) { Log.e(LOG_TAG, "JSON key 'url' was not a valid URL", ex); return null; } catch (JSONException ex) { Log.e(LOG_TAG, "JSON exception", ex); return null; } } @NonNull @Override public View createView(@NonNull BitmapAsset asset, @NonNull Context context) { ImageView imageView = new ImageView(context); imageView.setImageBitmap(asset.getBitmap()); imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); imageView.setAdjustViewBounds(true); return imageView; } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/handlers/ColorAssetHandler.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset.handlers; import android.content.Context; import android.os.Parcel; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import android.widget.FrameLayout; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.utilities.InvalidParcelException; import org.json.JSONException; import org.json.JSONObject; import java.net.URL; import java.util.Set; /** * Handles assets of Color type. */ public class ColorAssetHandler implements AssetManager.AssetHandler { /** * An asset implementation for a single static color */ static class ColorAsset implements Asset { public static final Creator CREATOR = new Creator() { @Override public ColorAsset createFromParcel(Parcel source) { return new ColorAsset(source); } @Override public ColorAsset[] newArray(int size) { return new ColorAsset[size]; } }; private final int color; private ColorAsset(int color) { this.color = color; } private ColorAsset(@NonNull Parcel parcel) { color = parcel.readInt(); } public int getColor() { return color; } @NonNull @Override public String getType() { return TYPE; } @Override public void validate() throws InvalidParcelException { // Nothing to validate } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(color); } } @SuppressWarnings("ViewConstructor") private static class ColorView extends FrameLayout { public ColorView(Context context, int color) { super(context); setBackgroundColor(color); } } public static final String TYPE = "Color"; private static final String LOG_TAG = ColorAssetHandler.class.getCanonicalName(); /** * Parses a string in the format '#RRGGBBAA', and returns it as an android-compatible color * * @param input The string to parse * @return The color, in an android-compatible format */ public static int fromRGBAHex(@Nullable String input) { if (input == null || input.equals("")) { return 0; } input = input.substring(1); try { long value = Long.parseLong(input, 16); // Android color has alpha first, not last. Flip the bytes around. return (int) ((value >> 8) | ((value & 0xFF) << 24)); } catch (NumberFormatException ex) { return 0; } } @Nullable @Override public Set getCacheURLs(@NonNull JSONObject payload) { return null; } @Nullable @Override public ColorAsset createAsset(@NonNull JSONObject payload, @NonNull AssetManager.AssetCache cache) { try { return new ColorAsset(fromRGBAHex(payload.getString("rgbaHex"))); } catch (JSONException ex) { Log.e(LOG_TAG, "JSON Exception", ex); return null; } } @NonNull @Override public View createView(@NonNull ColorAsset asset, @NonNull Context context) { return new ColorView(context, asset.getColor()); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/asset/handlers/GifAssetHandler.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.asset.handlers; import android.content.Context; import android.os.Parcel; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.utilities.GifDecoder; import com.facebook.notifications.internal.utilities.InvalidParcelException; import com.facebook.notifications.internal.view.GifView; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.HashSet; import java.util.Set; /** * Handles assets of the GIF type. */ public class GifAssetHandler implements AssetManager.AssetHandler { /** * A resource implementation for GIFs read from disk */ static class GifAsset implements Asset { public static final Creator CREATOR = new Creator() { @Override public GifAsset createFromParcel(Parcel source) { return new GifAsset(source); } @Override public GifAsset[] newArray(int size) { return new GifAsset[size]; } }; private final @NonNull File createdFrom; private transient @Nullable GifDecoder decoder; private GifAsset(@NonNull File createdFrom) { this.createdFrom = createdFrom; } private GifAsset(@NonNull Parcel source) { createdFrom = new File(source.readString()); } @Nullable private static GifDecoder decodeGif(@NonNull File file) { long fileLength = file.length(); if (fileLength == 0 || fileLength > Integer.MAX_VALUE) { return null; } byte[] bytes = new byte[(int) fileLength]; try { FileInputStream fileInputStream = new FileInputStream(file); int fileOffset = 0; try { while (true) { int remaining = (bytes.length - fileOffset); if (remaining == 0) { break; } int read = fileInputStream.read(bytes, fileOffset, remaining); if (read == -1) { throw new IOException("File was shorter than expected!"); } fileOffset += read; } } finally { fileInputStream.close(); } } catch (IOException ex) { Log.e(LOG_TAG, "IO Exception while reading GIF data", ex); return null; } GifDecoder decoder = new GifDecoder(); decoder.read(bytes); return decoder; } @NonNull public File getCreatedFrom() { return createdFrom; } @NonNull public GifDecoder getDecoder() { if (decoder == null) { decoder = decodeGif(createdFrom); if (decoder == null) { throw new RuntimeException("Failed to decode GIF"); } } return decoder; } @NonNull @Override public String getType() { return TYPE; } @Override public void validate() throws InvalidParcelException { if (!createdFrom.exists()) { throw new InvalidParcelException( new FileNotFoundException( "GIF cache file does not exist: " + createdFrom.getAbsolutePath() ) ); } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(createdFrom.getAbsolutePath()); } } public static final String TYPE = "GIF"; private static final String LOG_TAG = GifAssetHandler.class.getCanonicalName(); @Nullable @Override public Set getCacheURLs(@NonNull JSONObject payload) { try { URL url = new URL(payload.getString("url")); Set set = new HashSet<>(); set.add(url); return set; } catch (MalformedURLException ex) { return null; } catch (JSONException ex) { return null; } } @Nullable @Override public GifAsset createAsset(@NonNull JSONObject payload, @NonNull AssetManager.AssetCache cache) { try { URL url = new URL(payload.getString("url")); File cacheFile = cache.getCachedFile(url); if (cacheFile == null) { return null; } return new GifAsset(cacheFile); } catch (MalformedURLException ex) { Log.e(LOG_TAG, "JSON key 'url' was not a valid URL.", ex); return null; } catch (JSONException ex) { Log.e(LOG_TAG, "JSON exception", ex); return null; } } @NonNull @Override public View createView(@NonNull GifAsset asset, @NonNull Context context) { return new GifView(context, asset.getDecoder()); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/configuration/ActionConfiguration.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.configuration; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import com.facebook.notifications.internal.content.Content; import com.facebook.notifications.internal.content.TextContent; import org.json.JSONException; import org.json.JSONObject; import static com.facebook.notifications.internal.asset.handlers.ColorAssetHandler.fromRGBAHex; public class ActionConfiguration implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public ActionConfiguration createFromParcel(Parcel source) { return new ActionConfiguration(source); } @Override public ActionConfiguration[] newArray(int size) { return new ActionConfiguration[size]; } }; private final int backgroundColor; private final int borderColor; private final float borderWidth; private final @Nullable Content content; private final @Nullable Uri actionUri; public ActionConfiguration(JSONObject json) throws JSONException { backgroundColor = fromRGBAHex(json.optString("backgroundColor")); borderColor = fromRGBAHex(json.optString("borderColor")); borderWidth = (float) json.optDouble("borderWidth", 0); JSONObject contentJSON = json.optJSONObject("content"); content = contentJSON == null ? null : new TextContent(contentJSON); String jsonUri = json.optString("url", null); actionUri = jsonUri == null ? null : Uri.parse(jsonUri); } private ActionConfiguration(Parcel source) { ClassLoader classLoader = getClass().getClassLoader(); backgroundColor = source.readInt(); borderColor = source.readInt(); borderWidth = source.readFloat(); content = source.readParcelable(classLoader); actionUri = source.readParcelable(classLoader); } public int getBackgroundColor() { return backgroundColor; } public int getBorderColor() { return borderColor; } public float getBorderWidth() { return borderWidth; } @Nullable public Content getContent() { return content; } @Nullable public Uri getActionUri() { return actionUri; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(backgroundColor); dest.writeInt(borderColor); dest.writeFloat(borderWidth); dest.writeParcelable(content, flags); dest.writeParcelable(actionUri, flags); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/configuration/ActionsConfiguration.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.configuration; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.utilities.EnumCreator; import com.facebook.notifications.internal.utilities.InvalidParcelException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public class ActionsConfiguration implements Parcelable { public enum ActionsStyle implements Parcelable { Attached, Detached; public static final Creator CREATOR = new EnumCreator<>(ActionsStyle.class, values()); public static ActionsStyle parse(String input) { switch (input) { case "attached": return ActionsStyle.Attached; case "detached": return ActionsStyle.Detached; default: return ActionsStyle.Attached; } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } } public enum ActionsLayoutStyle implements Parcelable { Vertical, Horizontal; public static final Creator CREATOR = new EnumCreator<>(ActionsLayoutStyle.class, values()); public static ActionsLayoutStyle parse(String input) { switch (input) { case "vertical": return ActionsLayoutStyle.Vertical; case "horizontal": return ActionsLayoutStyle.Horizontal; default: return ActionsLayoutStyle.Vertical; } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } } public static final Creator CREATOR = new Creator() { @Override public ActionsConfiguration createFromParcel(Parcel source) { return new ActionsConfiguration(source); } @Override public ActionsConfiguration[] newArray(int size) { return new ActionsConfiguration[size]; } }; private final ActionsStyle style; private final ActionsLayoutStyle layoutStyle; private final Asset background; private final float topInset; private final float contentInset; private final float cornerRadius; private final @NonNull ActionConfiguration[] actions; private final float height; private ActionsConfiguration(@NonNull JSONObject json, @NonNull AssetManager assetManager) throws JSONException { style = ActionsStyle.parse(json.optString("style")); layoutStyle = ActionsLayoutStyle.parse(json.optString("layoutStyle")); background = assetManager.inflateAsset(json.optJSONObject("background")); topInset = (float) json.optDouble("topInset", 0); contentInset = (float) json.optDouble("contentInset", 0); cornerRadius = (float) json.optDouble("cornerRadius", 0); JSONArray rawActions = json.getJSONArray("actions"); actions = new ActionConfiguration[rawActions.length()]; for (int jsonIndex = 0; jsonIndex < actions.length; jsonIndex++) { actions[jsonIndex] = new ActionConfiguration(rawActions.getJSONObject(jsonIndex)); } height = (float) json.optDouble("height", 44); } private ActionsConfiguration(Parcel source) { ClassLoader loader = getClass().getClassLoader(); style = source.readParcelable(loader); layoutStyle = source.readParcelable(loader); background = source.readParcelable(loader); topInset = source.readFloat(); contentInset = source.readFloat(); cornerRadius = source.readFloat(); actions = source.createTypedArray(ActionConfiguration.CREATOR); height = source.readFloat(); } @Nullable public static ActionsConfiguration fromJSON(@Nullable JSONObject json, @NonNull AssetManager assetManager) throws JSONException { if (json == null) { return null; } return new ActionsConfiguration(json, assetManager); } public void validate() throws InvalidParcelException { if (background != null) { background.validate(); } } public ActionsStyle getStyle() { return style; } public ActionsLayoutStyle getLayoutStyle() { return layoutStyle; } public Asset getBackground() { return background; } public float getTopInset() { return topInset; } public float getContentInset() { return contentInset; } public float getCornerRadius() { return cornerRadius; } @NonNull public ActionConfiguration[] getActions() { return actions; } public float getHeight() { return height; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(style, flags); dest.writeParcelable(layoutStyle, flags); dest.writeParcelable(background, flags); dest.writeFloat(topInset); dest.writeFloat(contentInset); dest.writeFloat(cornerRadius); dest.writeTypedArray(actions, flags); dest.writeFloat(height); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/configuration/BodyConfiguration.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.configuration; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.content.Content; import com.facebook.notifications.internal.content.ContentManager; import com.facebook.notifications.internal.content.TextContent; import com.facebook.notifications.internal.utilities.InvalidParcelException; import org.json.JSONException; import org.json.JSONObject; public class BodyConfiguration implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public BodyConfiguration createFromParcel(Parcel source) { return new BodyConfiguration(source); } @Override public BodyConfiguration[] newArray(int size) { return new BodyConfiguration[size]; } }; private final @Nullable Asset background; private final @Nullable Content content; private BodyConfiguration( @NonNull JSONObject json, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager ) throws JSONException { background = assetManager.inflateAsset(json.getJSONObject("background")); // TODO: Go through content manager JSONObject contentJSON = json.optJSONObject("content"); content = contentJSON != null ? new TextContent(contentJSON) : null; } private BodyConfiguration(Parcel source) { ClassLoader loader = getClass().getClassLoader(); background = source.readParcelable(loader); content = source.readParcelable(loader); } @Nullable public static BodyConfiguration fromJSON(@Nullable JSONObject json, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager) throws JSONException { if (json == null) { return null; } return new BodyConfiguration(json, assetManager, contentManager); } public void validate() throws InvalidParcelException { if (background != null) { background.validate(); } } @Nullable public Asset getBackground() { return background; } @Nullable public Content getContent() { return content; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(background, flags); dest.writeParcelable(content, flags); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/configuration/CardConfiguration.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.configuration; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.asset.handlers.ColorAssetHandler; import com.facebook.notifications.internal.content.ContentManager; import com.facebook.notifications.internal.utilities.EnumCreator; import com.facebook.notifications.internal.utilities.InvalidParcelException; import org.json.JSONException; import org.json.JSONObject; public class CardConfiguration implements Parcelable { public enum CardSize implements Parcelable { Invalid, Small, Medium, Large; public static final Creator CREATOR = new EnumCreator<>(CardSize.class, values()); public static CardSize parse(String input) { switch (input) { case "small": return CardSize.Small; case "medium": return CardSize.Medium; case "large": return CardSize.Large; default: return CardSize.Invalid; } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } } private static final String LOG_TAG = CardConfiguration.class.getCanonicalName(); private final CardSize cardSize; private final float cornerRadius; private final float contentInset; private final int backdropColor; private final @Nullable HeroConfiguration heroConfiguration; private final @Nullable BodyConfiguration bodyConfiguration; private final @Nullable ActionsConfiguration actionsConfiguration; public static final Creator CREATOR = new Creator() { @Override public CardConfiguration createFromParcel(Parcel source) { try { CardConfiguration configuration = new CardConfiguration(source); configuration.validate(); return configuration; } catch (InvalidParcelException ex) { Log.w(LOG_TAG, "Failed to decode card configuration", ex); return null; } } @Override public CardConfiguration[] newArray(int size) { return new CardConfiguration[size]; } }; public CardConfiguration(@NonNull JSONObject jsonObject, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager) throws JSONException { cardSize = CardSize.parse(jsonObject.getString("size")); cornerRadius = (float) jsonObject.optDouble("cornerRadius", 0.0); contentInset = (float) jsonObject.optDouble("contentInset", 10.0); backdropColor = ColorAssetHandler.fromRGBAHex(jsonObject.getString("backdropColor")); heroConfiguration = HeroConfiguration.fromJSON(jsonObject.optJSONObject("hero"), assetManager, contentManager); bodyConfiguration = BodyConfiguration.fromJSON(jsonObject.optJSONObject("body"), assetManager, contentManager); actionsConfiguration = ActionsConfiguration.fromJSON(jsonObject.optJSONObject("actions"), assetManager); } private CardConfiguration(@NonNull Parcel source) { ClassLoader loader = getClass().getClassLoader(); cardSize = source.readParcelable(loader); cornerRadius = source.readFloat(); contentInset = source.readFloat(); backdropColor = source.readInt(); heroConfiguration = source.readParcelable(loader); bodyConfiguration = source.readParcelable(loader); actionsConfiguration = source.readParcelable(loader); } public void validate() throws InvalidParcelException { if (heroConfiguration != null) { heroConfiguration.validate(); } if (bodyConfiguration != null) { bodyConfiguration.validate(); } if (actionsConfiguration != null) { actionsConfiguration.validate(); } } public CardSize getCardSize() { return cardSize; } public float getCornerRadius() { return cornerRadius; } public float getContentInset() { return contentInset; } public int getBackdropColor() { return backdropColor; } @Nullable public HeroConfiguration getHeroConfiguration() { return heroConfiguration; } @Nullable public BodyConfiguration getBodyConfiguration() { return bodyConfiguration; } @Nullable public ActionsConfiguration getActionsConfiguration() { return actionsConfiguration; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(cardSize, flags); dest.writeFloat(cornerRadius); dest.writeFloat(contentInset); dest.writeInt(backdropColor); dest.writeParcelable(heroConfiguration, flags); dest.writeParcelable(bodyConfiguration, flags); dest.writeParcelable(actionsConfiguration, flags); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/configuration/HeroConfiguration.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.configuration; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.content.Content; import com.facebook.notifications.internal.content.ContentManager; import com.facebook.notifications.internal.content.TextContent; import com.facebook.notifications.internal.utilities.InvalidParcelException; import org.json.JSONException; import org.json.JSONObject; public class HeroConfiguration implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public HeroConfiguration createFromParcel(Parcel source) { return new HeroConfiguration(source); } @Override public HeroConfiguration[] newArray(int size) { return new HeroConfiguration[size]; } }; private final float height; private final @Nullable Asset background; private final @Nullable Content content; private final Content.VerticalAlignment contentVerticalAlignment; private HeroConfiguration( @NonNull JSONObject json, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager ) throws JSONException { height = (float) json.optDouble("height", -1); background = assetManager.inflateAsset(json.getJSONObject("background")); // TODO: Go through content manager JSONObject contentJSON = json.optJSONObject("content"); content = contentJSON != null ? new TextContent(contentJSON) : null; contentVerticalAlignment = Content.VerticalAlignment.parse(json.optString("contentAlign")); } private HeroConfiguration(Parcel parcel) { ClassLoader loader = getClass().getClassLoader(); height = parcel.readFloat(); background = parcel.readParcelable(loader); content = parcel.readParcelable(loader); contentVerticalAlignment = parcel.readParcelable(loader); } /** * Create a hero configuration from a possibly `null` JSON payload * * @param json JSON Payload. Can be null * @param assetManager Asset manager to use for any assets in the hero * @param contentManager Content manager to use for any content in the hero * @return A hero configuration if the JSON payload exists, or `null` if it does not * @throws JSONException if the JSON is in an invalid format */ @Nullable public static HeroConfiguration fromJSON( @Nullable JSONObject json, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager ) throws JSONException { if (json == null) { return null; } return new HeroConfiguration(json, assetManager, contentManager); } public void validate() throws InvalidParcelException { if (background != null) { background.validate(); } } public float getHeight() { return height; } @Nullable public Asset getBackground() { return background; } @Nullable public Content getContent() { return content; } public Content.VerticalAlignment getContentVerticalAlignment() { return contentVerticalAlignment; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeFloat(height); dest.writeParcelable(background, flags); dest.writeParcelable(content, flags); dest.writeParcelable(contentVerticalAlignment, flags); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/content/Content.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.content; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.view.View; import com.facebook.notifications.internal.utilities.EnumCreator; public interface Content extends Parcelable { enum VerticalAlignment implements Parcelable { Top, Center, Bottom; public static final Creator CREATOR = new EnumCreator<>(VerticalAlignment.class, values()); public static VerticalAlignment parse(String input) { switch (input) { case "top": return VerticalAlignment.Top; case "center": return VerticalAlignment.Center; case "bottom": return VerticalAlignment.Bottom; default: return VerticalAlignment.Center; } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } } /** * Attempt to apply this content to a given view * @param view The view to apply to */ void applyTo(@NonNull View view); } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/content/ContentManager.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.content; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import com.facebook.notifications.internal.utilities.InvalidParcelException; /** * Manager for content */ public class ContentManager implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public ContentManager createFromParcel(Parcel source) { return new ContentManager(source); } @Override public ContentManager[] newArray(int size) { return new ContentManager[size]; } }; // TODO: Extensible like AssetManager, but for content public ContentManager() { } public ContentManager(ContentManager other) { } public ContentManager(@NonNull Parcel source) { } public void setContext(@NonNull Context context) { } public void validate() throws InvalidParcelException { } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/content/TextContent.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.content; import android.graphics.Typeface; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.Gravity; import android.view.View; import android.widget.TextView; import com.facebook.notifications.internal.asset.handlers.ColorAssetHandler; import com.facebook.notifications.internal.utilities.EnumCreator; import com.facebook.notifications.internal.utilities.FontUtilities; import org.json.JSONException; import org.json.JSONObject; public class TextContent implements Content { public enum Alignment implements Parcelable { Left, Right, Center; public static final Creator CREATOR = new EnumCreator<>(Alignment.class, values()); public static Alignment parse(@NonNull String input) { switch (input) { case "left": return Left; case "right": return Right; case "center": return Center; default: return Left; } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } } public static final Creator CREATOR = new Creator() { @Override public TextContent createFromParcel(Parcel source) { return new TextContent(source); } @Override public TextContent[] newArray(int size) { return new TextContent[size]; } }; private final @NonNull String text; private final int textColor; private final @Nullable String typeface; private final float typefaceSize; private final Alignment textAlignment; public TextContent(@NonNull JSONObject json) throws JSONException { text = json.optString("text", ""); textColor = ColorAssetHandler.fromRGBAHex(json.optString("color")); typeface = json.optString("font"); typefaceSize = (float) json.optDouble("size", 18); // Default to 18sp, or 'medium' size. textAlignment = Alignment.parse(json.optString("align", "center")); } private TextContent(Parcel source) { text = source.readString(); textColor = source.readInt(); typeface = source.readString(); typefaceSize = source.readFloat(); textAlignment = source.readParcelable(getClass().getClassLoader()); } @Override public void applyTo(@NonNull View view) { if (view instanceof TextView) { TextView textView = (TextView)view; textView.setText(getText()); textView.setTextColor(getTextColor()); Typeface typeface = FontUtilities.parseFont(getTypeface()); typeface = typeface != null ? typeface : Typeface.DEFAULT; textView.setTypeface(typeface); textView.setTextSize(getTypefaceSize()); switch (getTextAlignment()) { case Left: textView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); break; case Center: textView.setGravity(Gravity.CENTER); break; case Right: textView.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL); break; } } } @NonNull public String getText() { return text; } public int getTextColor() { return textColor; } @Nullable public String getTypeface() { return typeface; } public float getTypefaceSize() { return typefaceSize; } public Alignment getTextAlignment() { return textAlignment; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(text); dest.writeInt(textColor); dest.writeString(typeface); dest.writeFloat(typefaceSize); dest.writeParcelable(textAlignment, flags); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/utilities/EnumCreator.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.utilities; import android.os.Parcel; import android.os.Parcelable; import java.lang.reflect.Array; public class EnumCreator implements Parcelable.Creator { private final Class kls; private final T[] values; public EnumCreator(Class kls, T[] values) { this.kls = kls; this.values = values; } @Override public T createFromParcel(Parcel source) { return values[source.readInt()]; } @Override @SuppressWarnings("unchecked") public T[] newArray(int size) { return (T[]) Array.newInstance(kls, size); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/utilities/FontUtilities.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.utilities; import android.graphics.Typeface; import android.support.annotation.Nullable; public class FontUtilities { public static @Nullable Typeface parseFont(@Nullable String fontName) { if (fontName == null) { return Typeface.DEFAULT; } switch (fontName.toLowerCase()) { case "system-regular": return Typeface.DEFAULT; case "system-light": return Typeface.create("sans-serif-light", Typeface.NORMAL); case "system-bold": return Typeface.DEFAULT_BOLD; case "system-italic": return Typeface.defaultFromStyle(Typeface.ITALIC); case "system-bolditalic": return Typeface.defaultFromStyle(Typeface.BOLD_ITALIC); default: return Typeface.create(fontName, Typeface.NORMAL); } } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/utilities/GifDecoder.java ================================================ /** * Copyright (c) 2013 Xcellent Creations, Inc. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook.notifications.internal.utilities; import android.graphics.Bitmap; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; /** * Reads frame data from a GIF image source and decodes it into individual frames * for animation purposes. Image data can be read from either and InputStream source * or a byte[]. * * This class is optimized for running animations with the frames, there * are no methods to get individual frame images, only to decode the next frame in the * animation sequence. Instead, it lowers its memory footprint by only housing the minimum * data necessary to decode the next frame in the animation sequence. * * The animation must be manually moved forward using {@link #advance()} before requesting the next * frame. This method must also be called before you request the first frame or an error will * occur. * * Implementation adapted from sample code published in Lyons. (2004). Java for Programmers, * republished under the MIT Open Source License */ @SuppressWarnings("ALL") public class GifDecoder { private static final String TAG = GifDecoder.class.getSimpleName(); /** * File read status: No errors. */ public static final int STATUS_OK = 0; /** * File read status: Error decoding file (may be partially decoded) */ public static final int STATUS_FORMAT_ERROR = 1; /** * File read status: Unable to open source. */ public static final int STATUS_OPEN_ERROR = 2; /** * max decoder pixel stack size */ protected static final int MAX_STACK_SIZE = 4096; /** * GIF Disposal Method meaning take no action */ private static final int DISPOSAL_UNSPECIFIED = 0; /** * GIF Disposal Method meaning leave canvas from previous frame */ private static final int DISPOSAL_NONE = 1; /** * GIF Disposal Method meaning clear canvas to background color */ private static final int DISPOSAL_BACKGROUND = 2; /** * GIF Disposal Method meaning clear canvas to frame before last */ private static final int DISPOSAL_PREVIOUS = 3; /** * Global status code of GIF data parsing */ protected int status; //Global File Header values and parsing flags protected int width; // full image width protected int height; // full image height protected boolean gctFlag; // global color table used protected int gctSize; // size of global color table protected int loopCount = 1; // iterations; 0 = repeat forever protected int[] gct; // global color table protected int[] act; // active color table protected int bgIndex; // background color index protected int bgColor; // background color protected int pixelAspect; // pixel aspect ratio protected boolean lctFlag; // local color table flag protected int lctSize; // local color table size // Raw GIF data from input source protected ByteBuffer rawData; // Raw data read working array protected byte[] block = new byte[256]; // current data block protected int blockSize = 0; // block size last graphic control extension info // LZW decoder working arrays protected short[] prefix; protected byte[] suffix; protected byte[] pixelStack; protected byte[] mainPixels; protected int[] mainScratch, copyScratch; protected ArrayList frames; // frames read from current file protected GifFrame currentFrame; protected Bitmap previousImage, currentImage, renderImage; protected int framePointer; protected int frameCount; /** * Inner model class housing metadata for each frame */ private static class GifFrame { public int ix, iy, iw, ih; /* Control Flags */ public boolean interlace; public boolean transparency; /* Disposal Method */ public int dispose; /* Transparency Index */ public int transIndex; /* Delay, in ms, to next frame */ public int delay; /* Index in the raw buffer where we need to start reading to decode */ public int bufferFrameStart; /* Local Color Table */ public int[] lct; } /** * Move the animation frame counter forward */ public void advance() { framePointer = (framePointer + 1) % frameCount; } /** * Recycle any bitmaps held by this GIF decoder, as well as any intermediate data. After * recycling, the decoder can no longer be used. */ public void recycle() { if (previousImage != null) { previousImage.recycle(); } if (currentImage != null) { currentImage.recycle(); } if (renderImage != null) { renderImage.recycle(); } gct = null; act = null; block = null; prefix = null; suffix = null; pixelStack = null; mainPixels = null; mainScratch = null; copyScratch = null; currentFrame = null; frames = null; } /** * Gets display duration for specified frame. * * @param n int index of frame * @return delay in milliseconds */ public int getDelay(int n) { int delay = -1; if ((n >= 0) && (n < frameCount)) { delay = frames.get(n).delay; } return delay; } /** * Gets display duration for the upcoming frame */ public int getNextDelay() { if (frameCount <=0 || framePointer < 0) { return -1; } return getDelay(framePointer); } /** * Gets the number of frames read from file. * * @return frame count */ public int getFrameCount() { return frameCount; } /** * Gets the current index of the animation frame, or -1 if animation hasn't not yet started * * @return frame index */ public int getCurrentFrameIndex() { return framePointer; } /** * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely. * * @return iteration count if one was specified, else 1. */ public int getLoopCount() { return loopCount; } public int getWidth() { return width; } public int getHeight() { return height; } /** * Get the next frame in the animation sequence. * * @return Bitmap representation of frame */ public Bitmap getNextFrame() { if (frameCount <= 0 || framePointer < 0 || currentImage == null) { return null; } GifFrame frame = frames.get(framePointer); //Set the appropriate color table if (frame.lct == null) { act = gct; } else { act = frame.lct; if (bgIndex == frame.transIndex) { bgColor = 0; } } int save = 0; if (frame.transparency) { save = act[frame.transIndex]; act[frame.transIndex] = 0; // set transparent color if specified } if (act == null) { Log.w(TAG, "No Valid Color Table"); status = STATUS_FORMAT_ERROR; // no color table defined return null; } setPixels(framePointer); // transfer pixel data to image // Reset the transparent pixel in the color table if (frame.transparency) { act[frame.transIndex] = save; } return currentImage; } /** * Reads GIF image from stream * * @param is containing GIF file. * @return read status code (0 = no errors) */ public int read(InputStream is, int contentLength) { long startTime = System.currentTimeMillis(); if (is != null) { try { int capacity = (contentLength > 0) ? (contentLength + 4096) : 4096; ByteArrayOutputStream buffer = new ByteArrayOutputStream(capacity); int nRead; byte[] data = new byte[16384]; while ((nRead = is.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } buffer.flush(); read(buffer.toByteArray()); } catch (IOException e) { Log.w(TAG, "Error reading data from stream", e); } } else { status = STATUS_OPEN_ERROR; } try { is.close(); } catch (Exception e) { Log.w(TAG, "Error closing stream", e); } return status; } /** * Reads GIF image from byte array * * @param data containing GIF file. * @return read status code (0 = no errors) */ public int read(byte[] data) { init(); if (data != null) { //Initiliaze the raw data buffer rawData = ByteBuffer.wrap(data); rawData.rewind(); rawData.order(ByteOrder.LITTLE_ENDIAN); readHeader(); if (!err()) { readContents(); if (frameCount < 0) { status = STATUS_FORMAT_ERROR; } } } else { status = STATUS_OPEN_ERROR; } return status; } /** * Creates new frame image from current data (and previous frames as specified by their disposition codes). */ protected void setPixels(int frameIndex) { GifFrame currentFrame = frames.get(frameIndex); GifFrame previousFrame = null; int previousIndex = frameIndex - 1; if (previousIndex >= 0) { previousFrame = frames.get(previousIndex); } // final location of blended pixels final int[] dest = mainScratch; // fill in starting image contents based on last image's dispose code if (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) { if (previousFrame.dispose == DISPOSAL_NONE && currentImage != null) { // Start with the current image currentImage.getPixels(dest, 0, width, 0, 0, width, height); } if (previousFrame.dispose == DISPOSAL_BACKGROUND) { // Start with a canvas filled with the background color int c = 0; if (!currentFrame.transparency) { c = bgColor; } for (int i = 0; i < previousFrame.ih; i++) { int n1 = (previousFrame.iy + i) * width + previousFrame.ix; int n2 = n1 + previousFrame.iw; for (int k = n1; k < n2; k++) { dest[k] = c; } } } if (previousFrame.dispose == DISPOSAL_PREVIOUS && previousImage != null) { // Start with the previous frame previousImage.getPixels(dest, 0, width, 0, 0, width, height); } } //Decode pixels for this frame into the global pixels[] scratch decodeBitmapData(currentFrame, mainPixels); // decode pixel data // copy each source line to the appropriate place in the destination int pass = 1; int inc = 8; int iline = 0; for (int i = 0; i < currentFrame.ih; i++) { int line = i; if (currentFrame.interlace) { if (iline >= currentFrame.ih) { pass++; switch (pass) { case 2: iline = 4; break; case 3: iline = 2; inc = 4; break; case 4: iline = 1; inc = 2; break; default: break; } } line = iline; iline += inc; } line += currentFrame.iy; if (line < height) { int k = line * width; int dx = k + currentFrame.ix; // start of line in dest int dlim = dx + currentFrame.iw; // end of dest line if ((k + width) < dlim) { dlim = k + width; // past dest edge } int sx = i * currentFrame.iw; // start of line in source while (dx < dlim) { // map color and insert in destination int index = ((int) mainPixels[sx++]) & 0xff; int c = act[index]; if (c != 0) { dest[dx] = c; } dx++; } } } //Copy pixels into previous image currentImage.getPixels(copyScratch, 0, width, 0, 0, width, height); previousImage.setPixels(copyScratch, 0, width, 0, 0, width, height); //Set pixels for current image currentImage.setPixels(dest, 0, width, 0, 0, width, height); } /** * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick. */ protected void decodeBitmapData(GifFrame frame, byte[] dstPixels) { long startTime = System.currentTimeMillis(); long stepOne, stepTwo, stepThree; if (frame != null) { //Jump to the frame start position rawData.position(frame.bufferFrameStart); } int nullCode = -1; int npix = (frame == null) ? width * height : frame.iw * frame.ih; int available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, count, i, datum, data_size, first, top, bi, pi; if (dstPixels == null || dstPixels.length < npix) { dstPixels = new byte[npix]; // allocate new pixel array } if (prefix == null) { prefix = new short[MAX_STACK_SIZE]; } if (suffix == null) { suffix = new byte[MAX_STACK_SIZE]; } if (pixelStack == null) { pixelStack = new byte[MAX_STACK_SIZE + 1]; } // Initialize GIF data stream decoder. data_size = read(); clear = 1 << data_size; end_of_information = clear + 1; available = clear + 2; old_code = nullCode; code_size = data_size + 1; code_mask = (1 << code_size) - 1; for (code = 0; code < clear; code++) { prefix[code] = 0; // XXX ArrayIndexOutOfBoundsException suffix[code] = (byte) code; } // Decode GIF pixel stream. datum = bits = count = first = top = pi = bi = 0; for (i = 0; i < npix; ) { if (top == 0) { if (bits < code_size) { // Load bytes until there are enough bits for a code. if (count == 0) { // Read a new data block. count = readBlock(); if (count <= 0) { break; } bi = 0; } datum += (((int) block[bi]) & 0xff) << bits; bits += 8; bi++; count--; continue; } // Get the next code. code = datum & code_mask; datum >>= code_size; bits -= code_size; // Interpret the code if ((code > available) || (code == end_of_information)) { break; } if (code == clear) { // Reset decoder. code_size = data_size + 1; code_mask = (1 << code_size) - 1; available = clear + 2; old_code = nullCode; continue; } if (old_code == nullCode) { pixelStack[top++] = suffix[code]; old_code = code; first = code; continue; } in_code = code; if (code == available) { pixelStack[top++] = (byte) first; code = old_code; } while (code > clear) { pixelStack[top++] = suffix[code]; code = prefix[code]; } first = ((int) suffix[code]) & 0xff; // Add a new string to the string table, if (available >= MAX_STACK_SIZE) { break; } pixelStack[top++] = (byte) first; prefix[available] = (short) old_code; suffix[available] = (byte) first; available++; if (((available & code_mask) == 0) && (available < MAX_STACK_SIZE)) { code_size++; code_mask += available; } old_code = in_code; } // Pop a pixel off the pixel stack. top--; dstPixels[pi++] = pixelStack[top]; i++; } for (i = pi; i < npix; i++) { dstPixels[i] = 0; // clear missing pixels } } /** * Returns true if an error was encountered during reading/decoding */ protected boolean err() { return status != STATUS_OK; } /** * Initializes or re-initializes reader */ protected void init() { status = STATUS_OK; frameCount = 0; framePointer = -1; frames = new ArrayList(); gct = null; } /** * Reads a single byte from the input stream. */ protected int read() { int curByte = 0; try { curByte = (rawData.get() & 0xFF); } catch (Exception e) { status = STATUS_FORMAT_ERROR; } return curByte; } /** * Reads next variable length block from input. * * @return number of bytes stored in "buffer" */ protected int readBlock() { blockSize = read(); int n = 0; if (blockSize > 0) { try { int count; while (n < blockSize) { count = blockSize - n; rawData.get(block, n, count); n += count; } } catch (Exception e) { Log.w(TAG, "Error Reading Block", e); status = STATUS_FORMAT_ERROR; } } return n; } /** * Reads color table as 256 RGB integer values * * @param ncolors int number of colors to read * @return int array containing 256 colors (packed ARGB with full alpha) */ protected int[] readColorTable(int ncolors) { int nbytes = 3 * ncolors; int[] tab = null; byte[] c = new byte[nbytes]; try { rawData.get(c); tab = new int[256]; // max size to avoid bounds checks int i = 0; int j = 0; while (i < ncolors) { int r = ((int) c[j++]) & 0xff; int g = ((int) c[j++]) & 0xff; int b = ((int) c[j++]) & 0xff; tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; } } catch (BufferUnderflowException e) { Log.w(TAG, "Format Error Reading Color Table", e); status = STATUS_FORMAT_ERROR; } return tab; } /** * Main file parser. Reads GIF content blocks. */ protected void readContents() { // read GIF file content blocks boolean done = false; while (!(done || err())) { int code = read(); switch (code) { case 0x2C: // image separator readBitmap(); break; case 0x21: // extension code = read(); switch (code) { case 0xf9: // graphics control extension //Start a new frame currentFrame = new GifFrame(); readGraphicControlExt(); break; case 0xff: // application extension readBlock(); String app = ""; for (int i = 0; i < 11; i++) { app += (char) block[i]; } if (app.equals("NETSCAPE2.0")) { readNetscapeExt(); } else { skip(); // don't care } break; case 0xfe:// comment extension skip(); break; case 0x01:// plain text extension skip(); break; default: // uninteresting extension skip(); } break; case 0x3b: // terminator done = true; break; case 0x00: // bad byte, but keep going and see what happens break; default: status = STATUS_FORMAT_ERROR; } } } /** * Reads GIF file header information. */ protected void readHeader() { String id = ""; for (int i = 0; i < 6; i++) { id += (char) read(); } if (!id.startsWith("GIF")) { status = STATUS_FORMAT_ERROR; return; } readLSD(); if (gctFlag && !err()) { gct = readColorTable(gctSize); bgColor = gct[bgIndex]; } } /** * Reads Graphics Control Extension values */ protected void readGraphicControlExt() { read(); // block size int packed = read(); // packed fields currentFrame.dispose = (packed & 0x1c) >> 2; // disposal method if (currentFrame.dispose == 0) { currentFrame.dispose = 1; // elect to keep old image if discretionary } currentFrame.transparency = (packed & 1) != 0; currentFrame.delay = readShort() * 10; // delay in milliseconds currentFrame.transIndex = read(); // transparent color index read(); // block terminator } /** * Reads next frame image */ protected void readBitmap() { currentFrame.ix = readShort(); // (sub)image position & size currentFrame.iy = readShort(); currentFrame.iw = readShort(); currentFrame.ih = readShort(); int packed = read(); lctFlag = (packed & 0x80) != 0; // 1 - local color table flag interlace lctSize = (int) Math.pow(2, (packed & 0x07) + 1); // 3 - sort flag // 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color // table size currentFrame.interlace = (packed & 0x40) != 0; if (lctFlag) { currentFrame.lct = readColorTable(lctSize); // read table } else { currentFrame.lct = null; //No local color table } currentFrame.bufferFrameStart = rawData.position(); //Save this as the decoding position pointer decodeBitmapData(null, mainPixels); // false decode pixel data to advance buffer skip(); if (err()) { return; } frameCount++; frames.add(currentFrame); // add image to frame } /** * Reads Logical Screen Descriptor */ protected void readLSD() { // logical screen size width = readShort(); height = readShort(); // packed fields int packed = read(); gctFlag = (packed & 0x80) != 0; // 1 : global color table flag // 2-4 : color resolution // 5 : gct sort flag gctSize = 2 << (packed & 7); // 6-8 : gct size bgIndex = read(); // background color index pixelAspect = read(); // pixel aspect ratio //Now that we know the size, init scratch arrays mainPixels = new byte[width * height]; mainScratch = new int[width * height]; copyScratch = new int[width * height]; previousImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); currentImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); } /** * Reads Netscape extenstion to obtain iteration count */ protected void readNetscapeExt() { do { readBlock(); if (block[0] == 1) { // loop count sub-block int b1 = ((int) block[1]) & 0xff; int b2 = ((int) block[2]) & 0xff; loopCount = (b2 << 8) | b1; } } while ((blockSize > 0) && !err()); } /** * Reads next 16-bit value, LSB first */ protected int readShort() { // read 16-bit value return rawData.getShort(); } /** * Skips variable length blocks up to and including next zero length block. */ protected void skip() { do { readBlock(); } while ((blockSize > 0) && !err()); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/utilities/InvalidParcelException.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.utilities; /** * Represents an exception that occurs when validating * the results of a object created from a parcel */ public class InvalidParcelException extends Exception { public InvalidParcelException() { } public InvalidParcelException(String detailMessage) { super(detailMessage); } public InvalidParcelException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } public InvalidParcelException(Throwable throwable) { super(throwable); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/utilities/JSONObjectVisitor.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.utilities; import org.json.JSONArray; import org.json.JSONObject; import java.util.Iterator; public abstract class JSONObjectVisitor { public static void walk(JSONObject object, JSONObjectVisitor visitor) { visitor.visit(object); } public static void walk(JSONArray array, JSONObjectVisitor visitor) { visitor.visit(array); } protected void visit(JSONObject object) { for (Iterator keys = object.keys(); keys.hasNext(); ) { String key = keys.next(); Object value = object.opt(key); if (value instanceof JSONObject) { visit((JSONObject) value); } if (value instanceof JSONArray) { visit((JSONArray) value); } } } protected void visit(JSONArray array) { for (int index = 0; index < array.length(); index++) { Object value = array.opt(index); if (value instanceof JSONObject) { visit((JSONObject) value); } if (value instanceof JSONArray) { visit((JSONArray) value); } } } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/utilities/RoundedViewHelper.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.utilities; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Path; import android.graphics.RectF; import android.support.annotation.NonNull; public class RoundedViewHelper { public static final int TOP_LEFT = 1 << 0; public static final int TOP_RIGHT = 1 << 1; public static final int BOTTOM_LEFT = 1 << 2; public static final int BOTTOM_RIGHT = 1 << 3; public static final int ALL = TOP_LEFT | TOP_RIGHT | BOTTOM_LEFT | BOTTOM_RIGHT; private final float cornerRadius; private final int corners; private final @NonNull RectF rect; private final @NonNull Path clipPath; /** * Create a new {@link RoundedViewHelper}, with a specified corner radius and corners. * * @param context The context to help inside (usually an {@link Activity}) * @param cornerRadiusDip The corner radius to use. * @param cornersMask The corners to automatically round. */ public RoundedViewHelper(@NonNull Context context, float cornerRadiusDip, int cornersMask) { corners = cornersMask; cornerRadius = context.getResources().getDisplayMetrics().density * cornerRadiusDip; rect = new RectF(); clipPath = new Path(); } public void onLayout(boolean changed, int l, int t, int r, int b) { if (corners == 0 || !changed) { return; } clipPath.reset(); rect.set(0, 0, (r - l), (b - t)); float[] radii = {0, 0, 0, 0, 0, 0, 0, 0}; if ((corners & TOP_LEFT) != 0) { radii[0] = cornerRadius; radii[1] = cornerRadius; } if ((corners & TOP_RIGHT) != 0) { radii[2] = cornerRadius; radii[3] = cornerRadius; } if ((corners & BOTTOM_LEFT) != 0) { radii[4] = cornerRadius; radii[5] = cornerRadius; } if ((corners & BOTTOM_RIGHT) != 0) { radii[6] = cornerRadius; radii[7] = cornerRadius; } clipPath.addRoundRect(rect, radii, Path.Direction.CW); } public void preDraw(Canvas canvas) { if (corners == 0) { return; } if (canvas.isOpaque()) { canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(), 255, Canvas.HAS_ALPHA_LAYER_SAVE_FLAG); } canvas.clipPath(clipPath); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/utilities/TransparentStateListDrawable.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.utilities; import android.graphics.PixelFormat; import android.graphics.drawable.StateListDrawable; /** * HACK: *

* Due to an optimization in how canvases work, drawables which have an 'opaque' opacity * setting actually still get immediately blitted to the screen, regardless of what canvas * they're drawing in, which means they ignore the clip path of the canvas they're drawing into. *

* We need a button that can be properly clipped, so we just simply extend the existing, working * drawable, and make it transparent. */ public class TransparentStateListDrawable extends StateListDrawable { @Override public int getOpacity() { return PixelFormat.TRANSPARENT; } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/utilities/Version.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.utilities; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.NoSuchElementException; import java.util.Scanner; /** * Simple version class */ public class Version implements Comparable { private final int major; private final int minor; private final int patch; public Version(int major, int minor, int patch) { this.major = major; this.minor = minor; this.patch = patch; } @Nullable public static Version parse(String input) { Scanner scanner = new Scanner(input); scanner.useDelimiter("\\."); int major = Integer.MIN_VALUE, minor = Integer.MIN_VALUE, patch = 0; try { major = scanner.nextInt(); minor = scanner.nextInt(); patch = scanner.nextInt(); } catch (NoSuchElementException ex) { if (major == Integer.MIN_VALUE && minor == Integer.MIN_VALUE) { return null; } } return new Version(major, minor, patch); } public int getPatch() { return patch; } public int getMinor() { return minor; } public int getMajor() { return major; } @Override public String toString() { return "Version{" + major + "." + minor + "." + patch + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Version version = (Version) o; return major == version.major && minor == version.minor && patch == version.patch; } @Override public int hashCode() { int result = major; result = 31 * result + minor; result = 31 * result + patch; return result; } @Override public int compareTo(@NonNull Version another) { int major = this.major - another.major; int minor = this.minor - another.minor; int patch = this.patch - another.patch; return major != 0 ? major : minor != 0 ? minor : patch; } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/view/ActionButton.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.view; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.support.annotation.NonNull; import android.util.DisplayMetrics; import android.util.StateSet; import android.widget.Button; import com.facebook.notifications.internal.configuration.ActionConfiguration; import com.facebook.notifications.internal.content.Content; import com.facebook.notifications.internal.utilities.TransparentStateListDrawable; @SuppressLint("ViewConstructor") public class ActionButton extends Button { public enum Type { Primary, Secondary, Dismiss } private final @NonNull ActionConfiguration configuration; private final Type type; public ActionButton(@NonNull Context context, @NonNull final ActionConfiguration config, @NonNull Type t, final float cornerRadius) { super(context, null, android.R.attr.borderlessButtonStyle); configuration = config; type = t; setTransformationMethod(null); setPadding(0, 0, 0, 0); final DisplayMetrics metrics = getResources().getDisplayMetrics(); final int backgroundColor = configuration.getBackgroundColor(); final int pressedColor; final int borderWidth = Math.round(configuration.getBorderWidth() * metrics.density); Content content = config.getContent(); if (content != null) { content.applyTo(this); } float[] hsv = {0, 0, 0}; Color.colorToHSV(backgroundColor, hsv); hsv[2] *= 0.5; pressedColor = Color.HSVToColor(backgroundColor >> 24, hsv); GradientDrawable backgroundGradient = new GradientDrawable() {{ setCornerRadius(cornerRadius * metrics.density); setShape(GradientDrawable.RECTANGLE); setStroke(borderWidth, configuration.getBorderColor()); setColor(backgroundColor); }}; GradientDrawable pressedGradient = new GradientDrawable() {{ setCornerRadius(cornerRadius * metrics.density); setShape(RECTANGLE); setStroke(borderWidth, configuration.getBorderColor()); setColor(pressedColor); }}; TransparentStateListDrawable stateListDrawable = new TransparentStateListDrawable(); stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, pressedGradient); stateListDrawable.addState(StateSet.WILD_CARD, backgroundGradient); setBackgroundDrawable(stateListDrawable); setWillNotDraw(false); // ClipPath is not hardware-accelerated before 4.3 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { setLayerType(LAYER_TYPE_SOFTWARE, null); } } @NonNull public ActionConfiguration getConfiguration() { return configuration; } public Type getType() { return type; } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/view/ActionsView.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.view; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.net.Uri; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.view.View; import android.widget.LinearLayout; import android.widget.RelativeLayout; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.configuration.ActionConfiguration; import com.facebook.notifications.internal.configuration.ActionsConfiguration; import com.facebook.notifications.internal.configuration.CardConfiguration; import com.facebook.notifications.internal.utilities.RoundedViewHelper; @SuppressWarnings("ResourceType") @SuppressLint("ViewConstructor") public class ActionsView extends RelativeLayout implements View.OnClickListener { public interface Delegate { void actionButtonClicked(ActionButton.Type type, @Nullable Uri actionUri); } private static final int ASSET_VIEW_ID = 0x78d5c27b; private static final int BUTTONS_LAYOUT_ID = 0x7cc9a8c8; private final @NonNull Delegate delegate; private final @Nullable ActionsConfiguration configuration; private final @NonNull RoundedViewHelper roundedViewHelper; private final @NonNull AssetView assetView; private final @NonNull LinearLayout buttonsLayout; private final @NonNull ActionButton[] actionButtons; public ActionsView(@NonNull Context context, @NonNull AssetManager assetManager, @NonNull Delegate del, @NonNull final CardConfiguration config) { super(context); delegate = del; configuration = config.getActionsConfiguration(); if (configuration == null) { roundedViewHelper = new RoundedViewHelper(context, 0, 0); assetView = new AssetView(context, assetManager, null); buttonsLayout = new LinearLayout(context); actionButtons = new ActionButton[0]; return; } roundedViewHelper = new RoundedViewHelper(context, config.getCornerRadius(), getRoundedCorners(config)); assetView = new AssetView(context, assetManager, configuration.getBackground()); ActionConfiguration[] actions = configuration.getActions(); actionButtons = new ActionButton[actions.length]; final DisplayMetrics metrics = getResources().getDisplayMetrics(); int margin = Math.round(configuration.getContentInset() * metrics.density); int topMargin = Math.round(configuration.getTopInset() * metrics.density); int marginLeft = 0, marginTop = 0; int buttonWidth = 0, buttonWeight = 0; int buttonHeight = Math.round(configuration.getHeight() * metrics.density); buttonsLayout = new LinearLayout(context); switch (configuration.getLayoutStyle()) { case Vertical: buttonWidth = LayoutParams.MATCH_PARENT; marginTop = margin; buttonsLayout.setOrientation(LinearLayout.VERTICAL); break; case Horizontal: buttonWeight = 1; marginLeft = margin; buttonsLayout.setOrientation(LinearLayout.HORIZONTAL); break; default: throw new RuntimeException("Unknown layout style: " + configuration.getLayoutStyle()); } boolean primary = true; for (int actionIndex = 0; actionIndex < actions.length; actionIndex++) { ActionConfiguration configuration = actions[actionIndex]; ActionButton.Type buttonType; if (configuration.getActionUri() != null) { buttonType = primary ? ActionButton.Type.Primary : ActionButton.Type.Secondary; primary = false; } else { buttonType = ActionButton.Type.Dismiss; } actionButtons[actionIndex] = new ActionButton(context, actions[actionIndex], buttonType, this.configuration.getCornerRadius()); actionButtons[actionIndex].setOnClickListener(this); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(buttonWidth, buttonHeight); layoutParams.weight = buttonWeight; if (actionIndex > 0) { layoutParams.setMargins(marginLeft, marginTop, 0, 0); } buttonsLayout.addView(actionButtons[actionIndex], layoutParams); } assetView.setId(ASSET_VIEW_ID); buttonsLayout.setId(BUTTONS_LAYOUT_ID); addView(assetView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) {{ addRule(ALIGN_TOP, BUTTONS_LAYOUT_ID); addRule(ALIGN_BOTTOM, BUTTONS_LAYOUT_ID); }}); buttonsLayout.setPadding(margin, topMargin, margin, margin); addView(buttonsLayout, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); setWillNotDraw(false); // ClipPath is not hardware-accelerated before 4.3 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { setLayerType(LAYER_TYPE_SOFTWARE, null); } } private static int getRoundedCorners(@NonNull CardConfiguration configuration) { if (configuration.getActionsConfiguration() == null) { return 0; } int corners = RoundedViewHelper.BOTTOM_LEFT | RoundedViewHelper.BOTTOM_RIGHT; if (configuration.getHeroConfiguration() == null && configuration.getBodyConfiguration() == null) { corners |= RoundedViewHelper.TOP_LEFT | RoundedViewHelper.TOP_RIGHT; } return corners; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); roundedViewHelper.onLayout(changed, l, t, r, b); } @Override public void draw(Canvas canvas) { roundedViewHelper.preDraw(canvas); super.draw(canvas); } @Override public void onClick(View v) { ActionButton button = (ActionButton) v; delegate.actionButtonClicked(button.getType(), button.getConfiguration().getActionUri()); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/view/AssetView.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.view; import android.annotation.SuppressLint; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.View; import android.widget.FrameLayout; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; /** * Implements a view that draws a asset. */ @SuppressLint("ViewConstructor") public class AssetView extends FrameLayout { private final @Nullable Asset asset; public AssetView(@NonNull Context context, @NonNull AssetManager assetManager, @Nullable Asset asset) { super(context); this.asset = asset; if (asset != null) { View viewForAsset = assetManager.inflateView(asset, context); addView(viewForAsset, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/view/BodyView.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.view; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.widget.RelativeLayout; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.configuration.ActionsConfiguration; import com.facebook.notifications.internal.configuration.BodyConfiguration; import com.facebook.notifications.internal.configuration.CardConfiguration; import com.facebook.notifications.internal.content.ContentManager; import com.facebook.notifications.internal.utilities.RoundedViewHelper; @SuppressWarnings("ResourceType") @SuppressLint("ViewConstructor") public class BodyView extends RelativeLayout { private final int RESOURCE_VIEW_ID = 0xa55d68f4; private final int CONTENT_VIEW_ID = 0x2facb6c8; private final @Nullable BodyConfiguration configuration; private final @NonNull RoundedViewHelper roundedViewHelper; private final @NonNull AssetView assetView; private final @NonNull ContentView contentView; public BodyView(@NonNull Context context, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager, @NonNull CardConfiguration config) { super(context); configuration = config.getBodyConfiguration(); ActionsConfiguration actionsConfiguration = config.getActionsConfiguration(); ActionsConfiguration.ActionsStyle actionsStyle = actionsConfiguration != null ? actionsConfiguration.getStyle() : ActionsConfiguration.ActionsStyle.Detached; int corners = actionsStyle == ActionsConfiguration.ActionsStyle.Detached ? RoundedViewHelper.BOTTOM_LEFT | RoundedViewHelper.BOTTOM_RIGHT : 0; if (config.getHeroConfiguration() == null && configuration != null) { corners |= RoundedViewHelper.TOP_LEFT | RoundedViewHelper.TOP_RIGHT; } roundedViewHelper = new RoundedViewHelper(context, config.getCornerRadius(), corners); if (configuration == null) { assetView = new AssetView(context, assetManager, null); contentView = new ContentView(context, contentManager, null); return; } assetView = new AssetView(context, assetManager, configuration.getBackground()); contentView = new ContentView(context, contentManager, configuration.getContent()); assetView.setId(RESOURCE_VIEW_ID); contentView.setId(CONTENT_VIEW_ID); DisplayMetrics metrics = getResources().getDisplayMetrics(); final int padding = Math.round(config.getContentInset() * metrics.density); contentView.setPadding(padding, padding, padding, padding); addView(assetView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) {{ addRule(ALIGN_TOP, CONTENT_VIEW_ID); addRule(ALIGN_BOTTOM, CONTENT_VIEW_ID); }}); addView(contentView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); setWillNotDraw(false); // ClipPath is not hardware-accelerated before 4.3 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { setLayerType(LAYER_TYPE_SOFTWARE, null); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); roundedViewHelper.onLayout(changed, l, t, r, b); } @Override public void draw(Canvas canvas) { roundedViewHelper.preDraw(canvas); super.draw(canvas); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/view/CardView.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.view; import android.annotation.SuppressLint; import android.content.Context; import android.support.annotation.NonNull; import android.util.DisplayMetrics; import android.view.Gravity; import android.widget.LinearLayout; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.configuration.CardConfiguration; import com.facebook.notifications.internal.configuration.HeroConfiguration; import com.facebook.notifications.internal.content.ContentManager; /** * This class is the start of the Push Card hierarchy. *

* It contains a {@link HeroView}, a {@link BodyView}, and a {@link ActionsView}. */ // As this class will only be created via code, suppress the following warning. @SuppressLint("ViewConstructor") public class CardView extends LinearLayout { private final @NonNull CardConfiguration configuration; private final @NonNull HeroView heroView; private final @NonNull BodyView bodyView; private final @NonNull ActionsView actionsView; /** * Create a new card view, from the given configuration. * * @param context The context of the view (usually an activity). * @param config The configuration of the push card to display. */ public CardView( @NonNull Context context, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager, @NonNull ActionsView.Delegate actionsDelegate, @NonNull CardConfiguration config ) { super(context); configuration = config; heroView = new HeroView(context, assetManager, contentManager, configuration); bodyView = new BodyView(context, assetManager, contentManager, configuration); actionsView = new ActionsView(context, assetManager, actionsDelegate, configuration); setOrientation(VERTICAL); setGravity(Gravity.CENTER_VERTICAL); addView(heroView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addView(bodyView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addView(actionsView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); setBackgroundColor(configuration.getBackdropColor()); } /** * Get a card size that fits for the given dimensions * * @param sizes An array of two floats (width and height) **measured in DIP**. * @param cardSize The target card size to fit * @return A new array with the proper card size for the given dimensions. */ private static @NonNull float[] sizeForCardSize(@NonNull float[] sizes, CardConfiguration.CardSize cardSize) { float layoutW = Math.min(400, sizes[0]); float layoutH = Math.min(700, sizes[1]); switch (cardSize) { case Invalid: return new float[]{0, 0}; case Small: layoutW *= 0.75; layoutH *= 0.7; break; case Medium: layoutW *= 0.83; layoutH *= 0.9; break; case Large: break; } return new float[]{layoutW, layoutH}; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { DisplayMetrics metrics = getResources().getDisplayMetrics(); int totalWidth = MeasureSpec.getSize(widthMeasureSpec); int totalHeight = MeasureSpec.getSize(heightMeasureSpec); float[] size = { totalWidth / metrics.density, totalHeight / metrics.density }; float[] cardSize = sizeForCardSize(size, configuration.getCardSize()); cardSize[0] *= metrics.density; cardSize[1] *= metrics.density; int hPadding = Math.round((totalWidth - cardSize[0]) / 2); setPadding(hPadding, 0, hPadding, 0); int measuredContentHeight = Integer.MAX_VALUE; HeroConfiguration heroConfiguration = configuration.getHeroConfiguration(); float heroHeight = 1; if (heroConfiguration == null || heroConfiguration.getHeight() == -1) { // Measure the total hero content size heroView.getLayoutParams().height = LayoutParams.WRAP_CONTENT; super.onMeasure(widthMeasureSpec, heightMeasureSpec); measuredContentHeight = heroView.getMeasuredHeight(); } else { heroHeight = heroConfiguration.getHeight(); } // Reset the hero view's height to zero so we can measure everything else heroView.getLayoutParams().height = 0; // NOTE: It is extremely important that this call is in the middle of the function. It feels // strange, but if we do not call it *after* we set our padding, it will calculate the width of // our children without taking the padding into account and cause things to flow off of the // screen. // Also note that android 6.0+ does not require an explicit second call to `onMeasure`, the View // subsystem appears to do it automatically whenever the padding is changed. super.onMeasure(widthMeasureSpec, heightMeasureSpec); int bodyHeight = bodyView.getMeasuredHeight(); int actionsHeight = actionsView.getMeasuredHeight(); int availableHeight = Math.round(cardSize[1]); int remainingHeight = availableHeight - bodyHeight - actionsHeight; int maxHeight = Math.round(remainingHeight * Math.abs(heroHeight)); heroView.getLayoutParams().height = Math.min(maxHeight, measuredContentHeight); // We must explicitly tell the view subsystem to re-calculate with the new hero height, // otherwise it will assume we want to keep the height of zero that we last measured with. super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/view/ContentView.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.view; import android.annotation.SuppressLint; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.widget.TextView; import com.facebook.notifications.internal.content.Content; import com.facebook.notifications.internal.content.ContentManager; /** * Implements a view that draws content. */ @SuppressLint("ViewConstructor") public class ContentView extends TextView { private final @Nullable Content content; public ContentView(@NonNull Context context, @NonNull ContentManager assetManager, @Nullable Content content) { super(context); this.content = content; if (content != null) { content.applyTo(this); } } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/view/GifView.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.view; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import com.facebook.notifications.internal.utilities.GifDecoder; @SuppressLint("ViewConstructor") public class GifView extends View { private class DecoderThread extends Thread { private final @Nullable GifDecoder decoder; private @Nullable Bitmap currentFrame; public DecoderThread(@Nullable GifDecoder decoder) { this.decoder = decoder; } @Override public void run() { if (decoder == null) { return; } while (true) { long frameStart = System.nanoTime(); decoder.advance(); int frameDelay = decoder.getNextDelay(); long targetNextFrame = frameStart + (frameDelay * 1000000); synchronized (this) { currentFrame = decoder.getNextFrame(); notifyAll(); } // Naive: assume all frames take the same amount of time to decode. long currentTime = System.nanoTime(); long frameDecodeTime = currentTime - frameStart; long sleepTimeNs = (targetNextFrame - currentTime) - frameDecodeTime; long sleepTimeMs = Math.max(0, sleepTimeNs / 1000000); postInvalidate(); try { Thread.sleep(sleepTimeMs); if (Thread.interrupted()) { break; } } catch (InterruptedException ex) { // Thread was killed by View, RIP. break; } } currentFrame = null; decoder.recycle(); } } private static final String LOG_TAG = GifView.class.getCanonicalName(); private final @NonNull DecoderThread decoderThread; private final @NonNull Paint antiAliasPaint; private final @NonNull Rect sourceRect; private final @NonNull RectF targetRect; public GifView(@NonNull Context context, @Nullable GifDecoder decoder) { super(context); setDrawingCacheEnabled(false); setWillNotCacheDrawing(true); decoderThread = new DecoderThread(decoder); antiAliasPaint = new Paint(); sourceRect = new Rect(); targetRect = new RectF(); antiAliasPaint.setAntiAlias(true); antiAliasPaint.setFilterBitmap(true); antiAliasPaint.setDither(true); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); decoderThread.start(); } @Override protected void onDetachedFromWindow() { decoderThread.interrupt(); try { decoderThread.join(); } catch (InterruptedException ex) { Log.e(LOG_TAG, "Failed to kill decoder thread", ex); } super.onDetachedFromWindow(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { GifDecoder decoder = decoderThread.decoder; if (decoder == null) { setMeasuredDimension(0, 0); return; } boolean variableWidth = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY; boolean variableHeight = MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); float sourceWidth = decoder.getWidth(); float sourceHeight = decoder.getHeight(); float destWidth = measuredWidth; float destHeight = measuredHeight; float heightRatio = destHeight / sourceHeight; float widthRatio = destWidth / sourceWidth; if (variableWidth && variableHeight) { float scale = Math.min(heightRatio, widthRatio); measuredWidth = Math.round(measuredWidth * scale); measuredHeight = Math.round(measuredHeight * scale); } else if (variableWidth) { measuredWidth = Math.round(measuredHeight * (sourceWidth / sourceHeight)); } else if (variableHeight) { measuredHeight = Math.round(measuredWidth * (sourceHeight / sourceWidth)); } setMeasuredDimension(measuredWidth, measuredHeight); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Bitmap currentFrame; synchronized (decoderThread) { currentFrame = decoderThread.currentFrame; if (currentFrame == null) { try { decoderThread.wait(100); currentFrame = decoderThread.currentFrame; } catch (InterruptedException e) { Log.e(LOG_TAG, "Failed to wait", e); } } } if (currentFrame == null) { return; } // Scale the frame that we have already in our bitmap and draw it. // This is the CENTER_CROP algorithm that we use for displaying images. float sourceWidth = currentFrame.getWidth(); float sourceHeight = currentFrame.getHeight(); float destWidth = getWidth(); float destHeight = getHeight(); float heightRatio = destHeight / sourceHeight; float widthRatio = destWidth / sourceWidth; float scale = Math.max(heightRatio, widthRatio); float targetWidth = sourceWidth * scale; float targetHeight = sourceHeight * scale; float offsetW = (destWidth - targetWidth) / 2f; float offsetH = (destHeight - targetHeight) / 2f; sourceRect.set(0, 0, currentFrame.getWidth(), currentFrame.getHeight()); targetRect.set(offsetW, offsetH, offsetW + targetWidth, offsetH + targetHeight); canvas.drawBitmap(currentFrame, sourceRect, targetRect, antiAliasPaint); } } ================================================ FILE: Android/notifications/src/main/java/com/facebook/notifications/internal/view/HeroView.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.internal.view; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.widget.RelativeLayout; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.configuration.ActionsConfiguration; import com.facebook.notifications.internal.configuration.CardConfiguration; import com.facebook.notifications.internal.configuration.HeroConfiguration; import com.facebook.notifications.internal.content.ContentManager; import com.facebook.notifications.internal.utilities.RoundedViewHelper; @SuppressWarnings("ResourceType") @SuppressLint("ViewConstructor") public class HeroView extends RelativeLayout { // Just some random generated IDs. private static final int ASSET_VIEW_ID = 0x53ec9fc7; private static final int CONTENT_VIEW_ID = 0xa5e76c35; private final @Nullable HeroConfiguration configuration; private final @NonNull RoundedViewHelper roundedViewHelper; private final @NonNull AssetView assetView; private final @NonNull ContentView contentView; private final int padding; public HeroView(@NonNull Context context, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager, @NonNull CardConfiguration config) { super(context); configuration = config.getHeroConfiguration(); roundedViewHelper = new RoundedViewHelper(context, config.getCornerRadius(), getRoundedCorners(config)); if (configuration == null) { assetView = new AssetView(context, assetManager, null); contentView = new ContentView(context, contentManager, null); padding = 0; return; } assetView = new AssetView(context, assetManager, configuration.getBackground()); contentView = new ContentView(context, contentManager, configuration.getContent()); assetView.setId(ASSET_VIEW_ID); contentView.setId(CONTENT_VIEW_ID); DisplayMetrics metrics = getResources().getDisplayMetrics(); padding = Math.round(config.getContentInset() * metrics.density); addView(assetView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addView(contentView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) {{ setMargins(padding, padding, padding, padding); switch (configuration.getContentVerticalAlignment()) { case Top: addRule(ALIGN_TOP, ASSET_VIEW_ID); break; case Center: addRule(CENTER_VERTICAL); break; case Bottom: addRule(ALIGN_BOTTOM, ASSET_VIEW_ID); break; } }}); setWillNotDraw(false); // ClipPath is not hardware-accelerated before 4.3 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { setLayerType(LAYER_TYPE_SOFTWARE, null); } } private static int getRoundedCorners(@NonNull CardConfiguration cardConfiguration) { if (cardConfiguration.getHeroConfiguration() == null) { return 0; } int corners = RoundedViewHelper.TOP_LEFT | RoundedViewHelper.TOP_RIGHT; if (cardConfiguration.getBodyConfiguration() == null && cardConfiguration.getActionsConfiguration() != null && cardConfiguration.getActionsConfiguration().getStyle() == ActionsConfiguration.ActionsStyle.Detached) { corners |= RoundedViewHelper.BOTTOM_LEFT | RoundedViewHelper.BOTTOM_RIGHT; } return corners; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { LayoutParams params = (LayoutParams) assetView.getLayoutParams(); // Params can return null if the assetView is not yet attached to the screen. if (params == null) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } params.height = LayoutParams.WRAP_CONTENT; super.onMeasure(widthMeasureSpec, heightMeasureSpec); int assetHeight = assetView.getMeasuredHeight(); int contentHeight = contentView.getMeasuredHeight() + (padding * 2); params.height = Math.max(assetHeight, contentHeight); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); roundedViewHelper.onLayout(changed, l, t, r, b); } @Override public void draw(Canvas canvas) { roundedViewHelper.preDraw(canvas); super.draw(canvas); } } ================================================ FILE: Android/notifications-example/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.3" defaultConfig { applicationId "com.facebook.notifications.sample" minSdkVersion 15 targetSdkVersion 23 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } repositories { mavenCentral() } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.android.support:design:23.1.1' compile 'com.google.android.gms:play-services:8.4.0' compile 'com.facebook.android:facebook-android-sdk:4.+' compile 'commons-io:commons-io:2.4' compile project(':notifications') apply plugin: 'com.google.gms.google-services' } ================================================ FILE: Android/notifications-example/google-services.json ================================================ { "project_info": { "project_id": "fbiantestapp", "project_number": "903525066365", "name": "FBIANTestApp" }, "client": [ { "client_info": { "mobilesdk_app_id": "1:903525066365:android:43f558a669a2aaaa", "client_id": "android:com.facebook.notifications.sample", "client_type": 1, "android_client_info": { "package_name": "com.facebook.notifications.sample", "certificate_hash": [] } }, "oauth_client": [], "api_key": [], "services": { "analytics_service": { "status": 1 }, "cloud_messaging_service": { "status": 2, "apns_config": [] }, "appinvite_service": { "status": 1, "other_platform_oauth_client": [] }, "google_signin_service": { "status": 1 }, "ads_service": { "status": 1 } } } ], "client_info": [], "ARTIFACT_VERSION": "1" } ================================================ FILE: Android/notifications-example/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/richardross/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: Android/notifications-example/src/main/AndroidManifest.xml ================================================ ================================================ FILE: Android/notifications-example/src/main/assets/example1.json ================================================ { "fb_push_card": { "version" : "1.0", "dismissColor": "#334D5CFF", "size": "small", "cornerRadius": 12.0, "contentInset" : 10, "backdropColor": "#000000CC", "hero": { "background": { "_type": "Color", "rgbaHex": "#FFFFFFFF" }, "content": { "_type": "StyledText", "size": 17, "text": "There are item(s) in your cart", "align": "left", "color": "#334D5CFF" }, "contentAlign" : "bottom" }, "body": { "background": { "_type": "Color", "rgbaHex": "#FFFFFFFF" }, "content": { "_type": "StyledText", "size": 15, "text": "Oops! It seems like you left some item(s) in your cart. You can remove them or checkout.", "align": "left", "color": "#334D5CFF" } }, "alert" : { "title": "Oops!", "body" : "There are item(s) in your cart!" }, "actions": { "style": "attached", "layoutStyle" : "vertical", "background": { "_type": "Color", "rgbaHex": "#FFFFFFFF" }, "contentInset" : 10, "cornerRadius" : 8, "actions": [ { "backgroundColor": "#61B6E5FF", "content": { "_type": "StyledText", "size": 18, "text": "GO TO CART", "color": "#FFFFFFFF" }, "url": "https://parse.com/" } ] } } } ================================================ FILE: Android/notifications-example/src/main/assets/example2.json ================================================ { "fb_push_payload" : { "campaign" : "2" }, "fb_push_card": { "version": "1.0", "size": "medium", "cornerRadius": 12, "contentInset" : 16, "backdropColor": "#334D5CF5", "hero": { "background": { "_type": "Color", "rgbaHex": "#50CCB4FF" }, "content": { "_type": "StyledText", "size": 24, "text": "Introducing Messaging!", "font": "system-bold", "align": "center", "color": "#FFFFFFFF" } }, "body": { "background": { "_type": "Color", "rgbaHex": "#FFFFFFFF" }, "content": { "_type": "StyledText", "size": 16, "text": "The AnyApp team is proud to launch our in app messaging feature! Give it a try today and message your friends.", "font": "system-light", "align": "left", "color": "#334D5CFF" } }, "alert" : { "body" : "Introducing Messaging!" }, "actions": { "style": "attached", "layoutStyle" : "horizontal", "actionsHeight" : 60.0, "actions": [ { "backgroundColor": "#C6D9E0FF", "title": "Not Now", "content": { "_type": "StyledText", "size": 18, "text": "Not Now", "color": "#FFFFFFFF" } }, { "backgroundColor": "#9167EFFF", "content": { "_type": "StyledText", "size": 18, "text": "Try It", "color": "#FFFFFFFF" }, "url": "https://messenger.com" } ] } } } ================================================ FILE: Android/notifications-example/src/main/assets/example3.json ================================================ { "fb_push_payload" : { "campaign" : "3" }, "fb_push_card": { "version": "1.0", "size": "medium", "cornerRadius": 16, "contentInset" : 16, "backdropColor": "#332D2DF0", "hero": { "background": { "_type": "GIF", "url": "https://s3.amazonaws.com/f.cl.ly/items/3S3a3I0a0l2w3M2v2h30/fall.gif" } }, "body": { "background": { "_type": "Color", "rgbaHex": "#FFFFFFFF" }, "content": { "_type": "StyledText", "size": 14, "text": "September 22nd marks the first day of fall and the release of our new fall clothing line. We’re celebrating with a 20% off sale!", "align": "left", "color": "#33251FFF" } }, "alert" : { "title": "New Clothing Line", "body" : "20% fall sale!" }, "actions": { "style": "attached", "layoutStyle" : "horizontal", "contentInset" : 16.0, "cornerRadius" : 6, "background": { "_type": "Color", "rgbaHex": "#FFFFFFFF" }, "actions": [ { "backgroundColor": "#690200FF", "content": { "_type": "StyledText", "size": 18, "text": "Yes, please!", "color": "#FFFFFFFF" }, "url": "https://parse.com/" }, { "backgroundColor": "#CCC4BCFF", "content": { "_type": "StyledText", "size": 18, "text": "No, thanks!", "color": "#FFFFFFFF" } } ] } } } ================================================ FILE: Android/notifications-example/src/main/assets/example4.json ================================================ { "fb_push_payload" : { "campaign" : "4" }, "fb_push_card": { "version": "1.0", "size": "medium", "cornerRadius": 20, "backdropColor": "#332D2DF0", "dismissColor": "#000000FF", "hero": { "background": { "_type": "Image", "url": "https://s3.amazonaws.com/f.cl.ly/items/3n2d2G3V3w2h261x0r0i/pizzzza.png" } }, "alert" : { "title": "Yum!", "body" : "Bob's now delivers!" }, "actions": { "style": "detached", "layoutStyle" : "horizontal", "topInset": 20, "cornerRadius": 20, "actions": [ { "borderColor": "#FFC84DFF", "borderWidth" : 2, "content": { "_type": "StyledText", "size": 18, "text": "Deliver Me Pizzza", "color": "#FFC84DFF" }, "url": "https://facebook.com/" } ] } } } ================================================ FILE: Android/notifications-example/src/main/assets/example5.json ================================================ { "fb_push_payload" : { "campaign" : "5" }, "fb_push_card": { "version": "1.0", "size": "large", "contentInset" : 20, "backdropColor": "#FFF9F8F5", "hero": { "background": { "_type": "Image", "url": "https://s3.amazonaws.com/f.cl.ly/items/2s0J0S1a0B2G1n0b421R/PirateBooty.png" } }, "alert" : { "title": "Yarr!", "body" : "Get the free pirate expansion!" }, "actions": { "style": "attached", "layoutStyle" : "vertical", "contentInset" : 10.0, "height" : 50, "cornerRadius" : 8, "actions": [ { "backgroundColor": "#EF5B5BFF", "content": { "_type": "StyledText", "size": 18, "text": "Get the free pirate expansion", "color": "#FFFFFFFF" }, "url": "http://worth1000.s3.amazonaws.com/submissions/384000/384447_858b_625x1000.jpg" }, { "backgroundColor": "#00000000", "content": { "_type": "StyledText", "size": 14, "text": "Nah, I don't like free", "color": "#EF5B5BFF" } } ] } } } ================================================ FILE: Android/notifications-example/src/main/java/com/facebook/notifications/sample/MainActivity.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.sample; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.View; import android.widget.Toast; import com.facebook.FacebookSdk; import com.facebook.notifications.NotificationCardResult; import com.facebook.notifications.NotificationsManager; import org.apache.commons.io.IOUtils; import org.json.JSONObject; import java.io.InputStream; import java.io.StringWriter; import java.nio.charset.Charset; public class MainActivity extends AppCompatActivity { private static final String LOG_TAG = MainActivity.class.getCanonicalName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FacebookSdk.sdkInitialize(getApplicationContext()); NotificationsManager.presentCardFromNotification(this); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); // register for GCM Intent intent = new Intent(this, RegistrationIntentService.class); startService(intent); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); NotificationsManager.presentCardFromNotification(this, intent); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); NotificationCardResult result = NotificationsManager.handleActivityResult(requestCode, resultCode, data); if (result != null) { Toast.makeText(this, "Result: " + result.getActionUri(), Toast.LENGTH_LONG).show(); } } /** * Mock an example push notification bundle from one of our local example JSON files. * @param exampleId The example id of the asset to load * @return a bundle with the contents of the specified example id */ @NonNull private Bundle getBundle(int exampleId) { try { InputStream inputStream = getAssets().open("example" + exampleId + ".json"); StringWriter output = new StringWriter(); IOUtils.copy(inputStream, output, Charset.forName("UTF-8")); JSONObject json = new JSONObject(output.toString()); Bundle bundle = new Bundle(); bundle.putString("fb_push_card", json.getJSONObject("fb_push_card").toString()); JSONObject pushPayload = json.optJSONObject("fb_push_payload"); if (pushPayload != null) { bundle.putString("fb_push_payload", pushPayload.toString()); } output.close(); inputStream.close(); return bundle; } catch (Exception ex) { Log.e(LOG_TAG, "Error while getting bundle", ex); return new Bundle(); } } public void showExample(View view) { Bundle exampleBundle = getBundle(Integer.parseInt(view.getTag().toString())); NotificationsManager.presentCard(this, exampleBundle); } public void showNotification(View view) { Bundle exampleBundle = getBundle(Integer.parseInt(view.getTag().toString())); NotificationsManager.presentNotification(this, exampleBundle, getIntent()); } } ================================================ FILE: Android/notifications-example/src/main/java/com/facebook/notifications/sample/MyGcmListenerService.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.sample; import android.content.Intent; import android.os.Bundle; import android.util.Log; import com.facebook.notifications.NotificationsManager; import com.google.android.gms.gcm.GcmListenerService; /** * Sample GCM listener service */ public class MyGcmListenerService extends GcmListenerService { private static final String LOG_TAG = MyGcmListenerService.class.getCanonicalName(); @Override public void onMessageReceived(String from, final Bundle data) { Log.v(LOG_TAG, "onMessageReceived(" + from + ", " + data + ")"); NotificationsManager.presentNotification( this, data, new Intent(getApplicationContext(), MainActivity.class) ); } } ================================================ FILE: Android/notifications-example/src/main/java/com/facebook/notifications/sample/MyInstanceIDListenerService.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.sample; import android.content.Intent; import com.google.android.gms.iid.InstanceIDListenerService; public class MyInstanceIDListenerService extends InstanceIDListenerService { @Override public void onTokenRefresh() { Intent intent = new Intent(this, RegistrationIntentService.class); startService(intent); } } ================================================ FILE: Android/notifications-example/src/main/java/com/facebook/notifications/sample/RegistrationIntentService.java ================================================ // Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications.sample; import android.app.IntentService; import android.content.Intent; import android.util.Log; import com.google.android.gms.gcm.GoogleCloudMessaging; import com.google.android.gms.iid.InstanceID; public class RegistrationIntentService extends IntentService { private static final String LOG_TAG = RegistrationIntentService.class.getCanonicalName(); public RegistrationIntentService() { super(LOG_TAG); } @Override protected void onHandleIntent(Intent intent) { try { synchronized (this) { InstanceID instanceID = InstanceID.getInstance(this); String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); Log.i(LOG_TAG, "GCM Registration Token: " + token); } } catch (Exception e) { Log.e(LOG_TAG, "Failed to complete token refresh", e); } } } ================================================ FILE: Android/notifications-example/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: Android/notifications-example/src/main/res/layout/content_main.xml ================================================