diff --git a/AndroidYahooWeather/.gitignore b/AndroidYahooWeather/.gitignore
new file mode 100644
index 0000000..d6bfc95
--- /dev/null
+++ b/AndroidYahooWeather/.gitignore
@@ -0,0 +1,4 @@
+.gradle
+/local.properties
+/.idea/workspace.xml
+.DS_Store
diff --git a/AndroidYahooWeather/build.gradle b/AndroidYahooWeather/build.gradle
new file mode 100644
index 0000000..9597164
--- /dev/null
+++ b/AndroidYahooWeather/build.gradle
@@ -0,0 +1,16 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:0.7.+'
+ }
+}
+
+allprojects {
+ repositories {
+ mavenCentral()
+ }
+}
diff --git a/AndroidYahooWeather/gradle.properties b/AndroidYahooWeather/gradle.properties
new file mode 100644
index 0000000..5d08ba7
--- /dev/null
+++ b/AndroidYahooWeather/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Settings specified in this file will override any Gradle settings
+# configured through the IDE.
+
+# 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
\ No newline at end of file
diff --git a/AndroidYahooWeather/gradle/wrapper/gradle-wrapper.jar b/AndroidYahooWeather/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
Binary files /dev/null and b/AndroidYahooWeather/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/AndroidYahooWeather/gradle/wrapper/gradle-wrapper.properties b/AndroidYahooWeather/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9b8ffdd
--- /dev/null
+++ b/AndroidYahooWeather/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.9-all.zip
diff --git a/AndroidYahooWeather/gradlew b/AndroidYahooWeather/gradlew
new file mode 100644
index 0000000..91a7e26
--- /dev/null
+++ b/AndroidYahooWeather/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/AndroidYahooWeather/gradlew.bat b/AndroidYahooWeather/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/AndroidYahooWeather/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/AndroidYahooWeather/settings.gradle b/AndroidYahooWeather/settings.gradle
new file mode 100644
index 0000000..b611c80
--- /dev/null
+++ b/AndroidYahooWeather/settings.gradle
@@ -0,0 +1 @@
+include ':weather'
diff --git a/AndroidYahooWeather/weather/.gitignore b/AndroidYahooWeather/weather/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/AndroidYahooWeather/weather/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/AndroidYahooWeather/weather/build.gradle b/AndroidYahooWeather/weather/build.gradle
new file mode 100644
index 0000000..433105d
--- /dev/null
+++ b/AndroidYahooWeather/weather/build.gradle
@@ -0,0 +1,24 @@
+apply plugin: 'android'
+
+android {
+ compileSdkVersion 17
+ buildToolsVersion '19.0.0'
+
+ defaultConfig {
+ minSdkVersion 11
+ targetSdkVersion 19
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ runProguard false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+}
+
+dependencies {
+ compile 'com.android.support:appcompat-v7:+'
+ compile files('libs/volley.jar')
+}
diff --git a/AndroidYahooWeather/weather/libs/add_volley_libe_here.txt b/AndroidYahooWeather/weather/libs/add_volley_libe_here.txt
new file mode 100644
index 0000000..e69de29
diff --git a/AndroidYahooWeather/weather/proguard-rules.txt b/AndroidYahooWeather/weather/proguard-rules.txt
new file mode 100644
index 0000000..5da226c
--- /dev/null
+++ b/AndroidYahooWeather/weather/proguard-rules.txt
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:/DevMobile/sdk1/eclipse_Android_SDK/android-sdk_r18-windows/android-sdk-windows/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# 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 *;
+#}
\ No newline at end of file
diff --git a/AndroidYahooWeather/weather/src/main/AndroidManifest.xml b/AndroidYahooWeather/weather/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0a7d7c5
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AndroidYahooWeather/weather/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/AndroidYahooWeather/weather/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
new file mode 100644
index 0000000..2a492f7
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.vending.billing;
+
+import android.os.Bundle;
+
+/**
+ * InAppBillingService is the service that provides in-app billing version 3 and beyond.
+ * This service provides the following features:
+ * 1. Provides a new API to get details of in-app items published for the app including
+ * price, type, title and description.
+ * 2. The purchase flow is synchronous and purchase information is available immediately
+ * after it completes.
+ * 3. Purchase information of in-app purchases is maintained within the Google Play system
+ * till the purchase is consumed.
+ * 4. An API to consume a purchase of an inapp item. All purchases of one-time
+ * in-app items are consumable and thereafter can be purchased again.
+ * 5. An API to get current purchases of the user immediately. This will not contain any
+ * consumed purchases.
+ *
+ * All calls will give a response code with the following possible values
+ * RESULT_OK = 0 - success
+ * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
+ * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
+ * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
+ * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
+ * RESULT_ERROR = 6 - Fatal error during the API action
+ * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
+ * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
+ */
+interface IInAppBillingService {
+ /**
+ * Checks support for the requested billing API version, package and in-app type.
+ * Minimum API version supported by this interface is 3.
+ * @param apiVersion the billing version which the app is using
+ * @param packageName the package name of the calling app
+ * @param type type of the in-app item being purchased "inapp" for one-time purchases
+ * and "subs" for subscription.
+ * @return RESULT_OK(0) on success, corresponding result code on failures
+ */
+ int isBillingSupported(int apiVersion, String packageName, String type);
+
+ /**
+ * Provides details of a list of SKUs
+ * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
+ * with a list JSON strings containing the productId, price, title and description.
+ * This API can be called with a maximum of 20 SKUs.
+ * @param apiVersion billing API version that the Third-party is using
+ * @param packageName the package name of the calling app
+ * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "DETAILS_LIST" with a StringArrayList containing purchase information
+ * in JSON format similar to:
+ * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
+ * "title : "Example Title", "description" : "This is an example description" }'
+ */
+ Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
+
+ /**
+ * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
+ * the type, a unique purchase token and an optional developer payload.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param sku the SKU of the in-app item as published in the developer console
+ * @param type the type of the in-app item ("inapp" for one-time purchases
+ * and "subs" for subscription).
+ * @param developerPayload optional argument to be sent back with the purchase information
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "BUY_INTENT" - PendingIntent to start the purchase flow
+ *
+ * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+ * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+ * If the purchase is successful, the result data will contain the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "INAPP_PURCHASE_DATA" - String in JSON format similar to
+ * '{"orderId":"12999763169054705758.1371079406387615",
+ * "packageName":"com.example.app",
+ * "productId":"exampleSku",
+ * "purchaseTime":1345678900000,
+ * "purchaseToken" : "122333444455555",
+ * "developerPayload":"example developer payload" }'
+ * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+ * was signed with the private key of the developer
+ * TODO: change this to app-specific keys.
+ */
+ Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
+ String developerPayload);
+
+ /**
+ * Returns the current SKUs owned by the user of the type and package name specified along with
+ * purchase information and a signature of the data to be validated.
+ * This will return all SKUs that have been purchased in V3 and managed items purchased using
+ * V1 and V2 that have not been consumed.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param type the type of the in-app items being requested
+ * ("inapp" for one-time purchases and "subs" for subscription).
+ * @param continuationToken to be set as null for the first call, if the number of owned
+ * skus are too many, a continuationToken is returned in the response bundle.
+ * This method can be called again with the continuation token to get the next set of
+ * owned skus.
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
+ * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
+ * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
+ * of the purchase information
+ * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+ * next set of in-app purchases. Only set if the
+ * user has more owned skus than the current list.
+ */
+ Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
+
+ /**
+ * Consume the last purchase of the given SKU. This will result in this item being removed
+ * from all subsequent responses to getPurchases() and allow re-purchase of this item.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param purchaseToken token in the purchase information JSON that identifies the purchase
+ * to be consumed
+ * @return 0 if consumption succeeded. Appropriate error values for failures.
+ */
+ int consumePurchase(int apiVersion, String packageName, String purchaseToken);
+}
diff --git a/AndroidYahooWeather/weather/src/main/ic_launcher-web.png b/AndroidYahooWeather/weather/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..4b14629
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/ic_launcher-web.png differ
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/Config.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/Config.java
new file mode 100644
index 0000000..519e12f
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/Config.java
@@ -0,0 +1,21 @@
+package com.survivingwithandroid.weather;
+
+/*
+ * Copyright (C) 2014 Francesco Azzola - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class Config {
+
+ public static int MAX_CITY_RESULT = 10;
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/MainActivity.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/MainActivity.java
new file mode 100644
index 0000000..852addd
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/MainActivity.java
@@ -0,0 +1,303 @@
+package com.survivingwithandroid.weather;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.preference.PreferenceManager;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.Volley;
+import com.survivingwithandroid.weather.billing.util.IabHelper;
+import com.survivingwithandroid.weather.billing.util.IabResult;
+import com.survivingwithandroid.weather.billing.util.SwABillingUtil;
+import com.survivingwithandroid.weather.model.Weather;
+import com.survivingwithandroid.weather.provider.IWeatherImageProvider;
+import com.survivingwithandroid.weather.provider.WeatherImageProvider;
+import com.survivingwithandroid.weather.provider.YahooClient;
+import com.survivingwithandroid.weather.settings.WeatherPreferenceActivity;
+
+/*
+ * Copyright (C) 2014 Francesco Azzola - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class MainActivity extends Activity {
+
+ private RequestQueue requestQueue;
+ private SharedPreferences prefs;
+ private MenuItem refreshItem;
+ TextView tvDescrWeather;
+ TextView tvLocation;
+ TextView tvTemperature;
+ TextView tvTempUnit;
+ TextView tvTempMin;
+ TextView tvTempMax;
+ TextView tvHum;
+ TextView tvWindSpeed;
+ TextView tvWindDeg;
+ TextView tvPress;
+ TextView tvPressStatus;
+ TextView tvVisibility;
+ ImageView weatherImage;
+ TextView tvSunrise;
+ TextView tvSunset;
+
+ private IabHelper mHelper ;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.d("SwA", "onCreate");
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.fragment_main);
+ requestQueue = Volley.newRequestQueue(getApplicationContext());
+ prefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ tvDescrWeather = (TextView) findViewById(R.id.descrWeather);
+ tvLocation = (TextView) findViewById(R.id.location);
+ tvTemperature = (TextView) findViewById(R.id.temp);
+ tvTempUnit = (TextView) findViewById(R.id.tempUnit);
+ tvTempMin = (TextView) findViewById(R.id.tempMin);
+ tvTempMax = (TextView) findViewById(R.id.tempMax);
+ tvHum = (TextView) findViewById(R.id.humidity);
+ tvWindSpeed = (TextView) findViewById(R.id.windSpeed);
+ tvWindDeg = (TextView) findViewById(R.id.windDeg);
+ tvPress = (TextView) findViewById(R.id.pressure);
+ tvPressStatus = (TextView) findViewById(R.id.pressureStat);
+ tvVisibility = (TextView) findViewById(R.id.visibility);
+ weatherImage = (ImageView) findViewById(R.id.imgWeather);
+ tvSunrise = (TextView) findViewById(R.id.sunrise);
+ tvSunset = (TextView) findViewById(R.id.sunset);
+
+ String base64EncodedPublicKey= SwABillingUtil.KEY;
+ mHelper = new IabHelper(this, base64EncodedPublicKey);
+ mHelper.enableDebugLogging(true);
+ mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
+ public void onIabSetupFinished(IabResult result) {
+ //Log.d("SwA", "Setup finished.");
+
+ if (!result.isSuccess()) {
+ // Oh noes, there was a problem.
+
+ Toast.makeText(MainActivity.this, "Billing error ["+result.getMessage()+"]", Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ // Have we been disposed of in the meantime? If so, quit.
+ if (mHelper == null) return;
+
+ // IAB is fully set up. Now, let's get an inventory of stuff we own.
+ // Log.d("SwA", "Setup successful.");
+ }
+ });
+
+ refreshData();
+
+ }
+
+
+
+ private void refreshData() {
+
+ if (prefs == null)
+ return ;
+
+
+ String woeid = prefs.getString("woeid", null);
+ //Log.d("SwA", "WOEID ["+woeid+"]");
+ if (woeid != null) {
+ String loc = prefs.getString("cityName", null) + "," + prefs.getString("country", null);
+ String unit = prefs.getString("swa_temp_unit", null);
+ handleProgressBar(true);
+
+ YahooClient.getWeather(woeid, unit, requestQueue, new YahooClient.WeatherClientListener() {
+ @Override
+ public void onWeatherResponse(Weather weather) {
+ //Log.d("SwA", "Weather ["+weather+"] - Cond ["+weather.condition+"] - Temp ["+weather.condition.temp+"]");
+ int code = weather.condition.code;
+
+ tvDescrWeather.setText(weather.condition.description);
+ tvLocation.setText(weather.location.name + "," + weather.location.region + "," + weather.location.country);
+ // Temperature data
+ tvTemperature.setText("" + weather.condition.temp);
+
+ int resId = getResource(weather.units.temperature, weather.condition.temp);
+ ( (TextView) findViewById(R.id.lineTxt)).setBackgroundResource(resId);
+
+ tvTempUnit.setText(weather.units.temperature);
+ tvTempMin.setText("" + weather.forecast.tempMin + " " + weather.units.temperature);
+ tvTempMax.setText("" + weather.forecast.tempMax + " " + weather.units.temperature);
+
+ // Humidity data
+ tvHum.setText("" + weather.atmosphere.humidity + " %");
+
+ // Wind data
+ tvWindSpeed.setText("" + weather.wind.speed + " " + weather.units.speed);
+ tvWindDeg.setText("" + weather.wind.direction + "°");
+
+ // Pressure data
+ tvPress.setText("" + weather.atmosphere.pressure + " " + weather.units.pressure);
+ int status = weather.atmosphere.rising;
+ String iconStat = "";
+ switch (status) {
+ case 0 :
+ iconStat = "-";
+ break;
+ case 1 :
+ iconStat = "+";
+ break;
+ case 2 :
+ iconStat = "-";
+ break;
+ };
+ tvPressStatus.setText(iconStat);
+
+ // Visibility
+ tvVisibility.setText("" + weather.atmosphere.visibility + " " + weather.units.distance);
+
+ // Astronomy
+ tvSunrise.setText(weather.astronomy.sunRise);
+ tvSunset.setText(weather.astronomy.sunSet);
+
+ // Update
+
+ IWeatherImageProvider provider = new WeatherImageProvider();
+ provider.getImage(code, requestQueue, new IWeatherImageProvider.WeatherImageListener() {
+ @Override
+ public void onImageReady(Bitmap image) {
+ weatherImage.setImageBitmap(image);
+ }
+ });
+ handleProgressBar(false);
+ }
+ });
+
+
+ }
+ }
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ //Log.d("SwA", "onActivityResult(" + requestCode + "," + resultCode + "," + data);
+ if (mHelper == null) return;
+
+ // Pass on the activity result to the helper for handling
+ if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
+ // not handled, so handle it ourselves (here's where you'd
+ // perform any handling of activity results not related to in-app
+ // billing...
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ else {
+ // Log.d("SwA", "onActivityResult handled by IABUtil.");
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+ if (id == R.id.action_settings) {
+ Intent i = new Intent();
+ i.setClass(this, WeatherPreferenceActivity.class);
+ startActivity(i);
+ }
+ else if (id == R.id.action_refresh) {
+ refreshItem = item;
+ refreshData();
+ }
+ else if (id == R.id.action_share) {
+ String playStoreLink = "https://play.google.com/store/apps/details?id=" +
+ getPackageName();
+
+ String msg = getResources().getString(R.string.share_msg) + playStoreLink;
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, msg);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+ else if (id == R.id.action_donate) {
+ SwABillingUtil.showDonateDialog(this, mHelper, this);
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mHelper != null) mHelper.dispose();
+ mHelper = null;
+ }
+
+
+
+ private void handleProgressBar(boolean visible) {
+ if (refreshItem == null)
+ return ;
+
+ if (visible)
+ refreshItem.setActionView(R.layout.progress_bar);
+ else
+ refreshItem.setActionView(null);
+ }
+
+
+ private float convertToC(String unit, float val) {
+ if (unit.equalsIgnoreCase("°C"))
+ return val;
+
+ return (float) ((val - 32) / 1.8);
+ }
+
+ private int getResource(String unit, float val) {
+ float temp = convertToC(unit, val);
+ Log.d("SwA", "Temp ["+temp+"]");
+ int resId = 0;
+ if (temp < 10)
+ resId = R.drawable.line_shape_blue;
+ else if (temp >= 10 && temp <=24)
+ resId = R.drawable.line_shape_green;
+ else if (temp > 25)
+ resId = R.drawable.line_shape_red;
+
+ return resId;
+
+ }
+
+
+
+
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Base64.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Base64.java
new file mode 100644
index 0000000..1db3548
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Base64.java
@@ -0,0 +1,570 @@
+// Portions copyright 2002, Google, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.survivingwithandroid.weather.billing.util;
+
+// This code was converted from code at http://iharder.sourceforge.net/base64/
+// Lots of extraneous features were removed.
+/* The original code said:
+ *
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit
+ * http://iharder.net/xmlizable
+ * periodically to check for updates or to contribute improvements.
+ *
+ *
+ * @author Robert Harder
+ * @author rharder@usa.net
+ * @version 1.3
+ */
+
+/**
+ * Base64 converter class. This code is not a complete MIME encoder;
+ * it simply converts binary data to base64 data and back.
+ *
+ *
Note {@link CharBase64} is a GWT-compatible implementation of this
+ * class.
+ */
+public class Base64 {
+ /** Specify encoding (value is {@code true}). */
+ public final static boolean ENCODE = true;
+
+ /** Specify decoding (value is {@code false}). */
+ public final static boolean DECODE = false;
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte) '\n';
+
+ /**
+ * The 64 valid Base64 values.
+ */
+ private final static byte[] ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '+', (byte) '/'};
+
+ /**
+ * The 64 valid web safe Base64 values.
+ */
+ private final static byte[] WEBSAFE_ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '-', (byte) '_'};
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ **/
+ private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9, -9, -9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ /** The web safe decodabet */
+ private final static byte[] WEBSAFE_DECODABET =
+ {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
+ 62, // Dash '-' sign at decimal 45
+ -9, -9, // Decimal 46-47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, // Decimal 91-94
+ 63, // Underscore '_' at decimal 95
+ -9, // Decimal 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ // Indicates white space in encoding
+ private final static byte WHITE_SPACE_ENC = -5;
+ // Indicates equals sign in encoding
+ private final static byte EQUALS_SIGN_ENC = -1;
+
+ /** Defeats instantiation. */
+ private Base64() {
+ }
+
+ /* ******** E N C O D I N G M E T H O D S ******** */
+
+ /**
+ * Encodes up to three bytes of the array source
+ * and writes the resulting four Base64 bytes to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 3 for
+ * the source array or destOffset + 4 for
+ * the destination array.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param alphabet is the encoding alphabet
+ * @return the destination array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset,
+ int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index alphabet
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff =
+ (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
+ return destination;
+ case 2:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ case 1:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Equivalent to calling
+ * {@code encodeBytes(source, 0, source.length)}
+ *
+ * @param source The data to convert
+ * @since 1.4
+ */
+ public static String encode(byte[] source) {
+ return encode(source, 0, source.length, ALPHABET, true);
+ }
+
+ /**
+ * Encodes a byte array into web safe Base64 notation.
+ *
+ * @param source The data to convert
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ */
+ public static String encodeWebSafe(byte[] source, boolean doPadding) {
+ return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source the data to convert
+ * @param off offset in array where conversion should begin
+ * @param len length of data to convert
+ * @param alphabet the encoding alphabet
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ * @since 1.4
+ */
+ public static String encode(byte[] source, int off, int len, byte[] alphabet,
+ boolean doPadding) {
+ byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
+ int outLen = outBuff.length;
+
+ // If doPadding is false, set length to truncate '='
+ // padding characters
+ while (doPadding == false && outLen > 0) {
+ if (outBuff[outLen - 1] != '=') {
+ break;
+ }
+ outLen -= 1;
+ }
+
+ return new String(outBuff, 0, outLen);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source the data to convert
+ * @param off offset in array where conversion should begin
+ * @param len length of data to convert
+ * @param alphabet is the encoding alphabet
+ * @param maxLineLength maximum length of one line.
+ * @return the BASE64-encoded byte array
+ */
+ public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
+ int maxLineLength) {
+ int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
+ int len43 = lenDiv3 * 4;
+ byte[] outBuff = new byte[len43 // Main 4:3
+ + (len43 / maxLineLength)]; // New lines
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for (; d < len2; d += 3, e += 4) {
+
+ // The following block of code is the same as
+ // encode3to4( source, d + off, 3, outBuff, e, alphabet );
+ // but inlined for faster encoding (~20% improvement)
+ int inBuff =
+ ((source[d + off] << 24) >>> 8)
+ | ((source[d + 1 + off] << 24) >>> 16)
+ | ((source[d + 2 + off] << 24) >>> 24);
+ outBuff[e] = alphabet[(inBuff >>> 18)];
+ outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // end for: each piece of array
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e, alphabet);
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ // Add a last newline
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ }
+ e += 4;
+ }
+
+ assert (e == outBuff.length);
+ return outBuff;
+ }
+
+
+ /* ******** D E C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Decodes four bytes from array source
+ * and writes the resulting bytes (up to three of them)
+ * to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 4 for
+ * the source array or destOffset + 3 for
+ * the destination array.
+ * This method returns the actual number of bytes that
+ * were converted from the Base64 encoding.
+ *
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3(byte[] source, int srcOffset,
+ byte[] destination, int destOffset, byte[] decodabet) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ } else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ // Example: DkL=
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ } else {
+ // Example: DkLE
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
+ | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+ return 3;
+ }
+ } // end decodeToBytes
+
+
+ /**
+ * Decodes data from Base64 notation.
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decode(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes data from web safe Base64 notation.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decodeWebSafe(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source The Base64 encoded data
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source) throws Base64DecoderException {
+ return decode(source, 0, source.length);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded data.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source)
+ throws Base64DecoderException {
+ return decodeWebSafe(source, 0, source.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, DECODABET);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded byte array.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @return decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, WEBSAFE_DECODABET);
+ }
+
+ /**
+ * Decodes Base64 content using the supplied decodabet and returns
+ * the decoded byte array.
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return decoded data
+ */
+ public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
+ throws Base64DecoderException {
+ int len34 = len * 3 / 4;
+ byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+ int i = 0;
+ byte sbiCrop = 0;
+ byte sbiDecode = 0;
+ for (i = 0; i < len; i++) {
+ sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
+ sbiDecode = decodabet[sbiCrop];
+
+ if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
+ if (sbiDecode >= EQUALS_SIGN_ENC) {
+ // An equals sign (for padding) must not occur at position 0 or 1
+ // and must be the last byte[s] in the encoded value
+ if (sbiCrop == EQUALS_SIGN) {
+ int bytesLeft = len - i;
+ byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
+ if (b4Posn == 0 || b4Posn == 1) {
+ throw new Base64DecoderException(
+ "invalid padding byte '=' at byte offset " + i);
+ } else if ((b4Posn == 3 && bytesLeft > 2)
+ || (b4Posn == 4 && bytesLeft > 1)) {
+ throw new Base64DecoderException(
+ "padding byte '=' falsely signals end of encoded value "
+ + "at offset " + i);
+ } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
+ throw new Base64DecoderException(
+ "encoded value has invalid trailing byte");
+ }
+ break;
+ }
+
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn == 4) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ b4Posn = 0;
+ }
+ }
+ } else {
+ throw new Base64DecoderException("Bad Base64 input character at " + i
+ + ": " + source[i + off] + "(decimal)");
+ }
+ }
+
+ // Because web safe encoding allows non padding base64 encodes, we
+ // need to pad the rest of the b4 buffer with equal signs when
+ // b4Posn != 0. There can be at most 2 equal signs at the end of
+ // four characters, so the b4 buffer must have two or three
+ // characters. This also catches the case where the input is
+ // padded with EQUALS_SIGN
+ if (b4Posn != 0) {
+ if (b4Posn == 1) {
+ throw new Base64DecoderException("single trailing character at offset "
+ + (len - 1));
+ }
+ b4[b4Posn++] = EQUALS_SIGN;
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ }
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Base64DecoderException.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Base64DecoderException.java
new file mode 100644
index 0000000..d3c0f18
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Base64DecoderException.java
@@ -0,0 +1,32 @@
+// Copyright 2002, Google, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.survivingwithandroid.weather.billing.util;
+
+/**
+ * Exception thrown when encountering an invalid Base64 input character.
+ *
+ * @author nelson
+ */
+public class Base64DecoderException extends Exception {
+ public Base64DecoderException() {
+ super();
+ }
+
+ public Base64DecoderException(String s) {
+ super(s);
+ }
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabException.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabException.java
new file mode 100644
index 0000000..56407d9
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabException.java
@@ -0,0 +1,43 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.survivingwithandroid.weather.billing.util;
+
+/**
+ * Exception thrown when something went wrong with in-app billing.
+ * An IabException has an associated IabResult (an error).
+ * To get the IAB result that caused this exception to be thrown,
+ * call {@link #getResult()}.
+ */
+public class IabException extends Exception {
+ IabResult mResult;
+
+ public IabException(IabResult r) {
+ this(r, null);
+ }
+ public IabException(int response, String message) {
+ this(new IabResult(response, message));
+ }
+ public IabException(IabResult r, Exception cause) {
+ super(r.getMessage(), cause);
+ mResult = r;
+ }
+ public IabException(int response, String message, Exception cause) {
+ this(new IabResult(response, message), cause);
+ }
+
+ /** Returns the IAB result (error) that this exception signals. */
+ public IabResult getResult() { return mResult; }
+}
\ No newline at end of file
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabHelper.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabHelper.java
new file mode 100644
index 0000000..ac4f9b9
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabHelper.java
@@ -0,0 +1,991 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.survivingwithandroid.weather.billing.util;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.vending.billing.IInAppBillingService;
+
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides convenience methods for in-app billing. You can create one instance of this
+ * class for your application and use it to process in-app billing operations.
+ * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
+ * many common in-app billing operations, as well as automatic signature
+ * verification.
+ *
+ * After instantiating, you must perform setup in order to start using the object.
+ * To perform setup, call the {@link #startSetup} method and provide a listener;
+ * that listener will be notified when setup is complete, after which (and not before)
+ * you may call other methods.
+ *
+ * After setup is complete, you will typically want to request an inventory of owned
+ * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
+ * and related methods.
+ *
+ * When you are done with this object, don't forget to call {@link #dispose}
+ * to ensure proper cleanup. This object holds a binding to the in-app billing
+ * service, which will leak unless you dispose of it correctly. If you created
+ * the object on an Activity's onCreate method, then the recommended
+ * place to dispose of it is the Activity's onDestroy method.
+ *
+ * A note about threading: When using this object from a background thread, you may
+ * call the blocking versions of methods; when using from a UI thread, call
+ * only the asynchronous versions and handle the results via callbacks.
+ * Also, notice that you can only call one asynchronous operation at a time;
+ * attempting to start a second asynchronous operation while the first one
+ * has not yet completed will result in an exception being thrown.
+ *
+ * @author Bruno Oliveira (Google)
+ *
+ */
+public class IabHelper {
+ // Is debug logging enabled?
+ boolean mDebugLog = false;
+ String mDebugTag = "IabHelper";
+
+ // Is setup done?
+ boolean mSetupDone = false;
+
+ // Has this object been disposed of? (If so, we should ignore callbacks, etc)
+ boolean mDisposed = false;
+
+ // Are subscriptions supported?
+ boolean mSubscriptionsSupported = false;
+
+ // Is an asynchronous operation in progress?
+ // (only one at a time can be in progress)
+ boolean mAsyncInProgress = false;
+
+ // (for logging/debugging)
+ // if mAsyncInProgress == true, what asynchronous operation is in progress?
+ String mAsyncOperation = "";
+
+ // Context we were passed during initialization
+ Context mContext;
+
+ // Connection to the service
+ IInAppBillingService mService;
+ ServiceConnection mServiceConn;
+
+ // The request code used to launch purchase flow
+ int mRequestCode;
+
+ // The item type of the current purchase flow
+ String mPurchasingItemType;
+
+ // Public key for verifying signature, in base64 encoding
+ String mSignatureBase64 = null;
+
+ // Billing response codes
+ public static final int BILLING_RESPONSE_RESULT_OK = 0;
+ public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
+ public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
+ public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
+ public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
+
+ // IAB Helper error codes
+ public static final int IABHELPER_ERROR_BASE = -1000;
+ public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
+ public static final int IABHELPER_BAD_RESPONSE = -1002;
+ public static final int IABHELPER_VERIFICATION_FAILED = -1003;
+ public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
+ public static final int IABHELPER_USER_CANCELLED = -1005;
+ public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
+ public static final int IABHELPER_MISSING_TOKEN = -1007;
+ public static final int IABHELPER_UNKNOWN_ERROR = -1008;
+ public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
+ public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
+
+ // Keys for the responses from InAppBillingService
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
+ public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
+ public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
+ public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
+ public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
+ public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
+
+ // Item types
+ public static final String ITEM_TYPE_INAPP = "inapp";
+ public static final String ITEM_TYPE_SUBS = "subs";
+
+ // some fields on the getSkuDetails response bundle
+ public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
+ public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
+
+ /**
+ * Creates an instance. After creation, it will not yet be ready to use. You must perform
+ * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
+ * block and is safe to call from a UI thread.
+ *
+ * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
+ * @param base64PublicKey Your application's public key, encoded in base64.
+ * This is used for verification of purchase signatures. You can find your app's base64-encoded
+ * public key in your application's page on Google Play Developer Console. Note that this
+ * is NOT your "developer public key".
+ */
+ public IabHelper(Context ctx, String base64PublicKey) {
+ mContext = ctx.getApplicationContext();
+ mSignatureBase64 = base64PublicKey;
+ logDebug("IAB helper created.");
+ }
+
+ /**
+ * Enables or disable debug logging through LogCat.
+ */
+ public void enableDebugLogging(boolean enable, String tag) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ mDebugTag = tag;
+ }
+
+ public void enableDebugLogging(boolean enable) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ }
+
+ /**
+ * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
+ * when the setup process is complete.
+ */
+ public interface OnIabSetupFinishedListener {
+ /**
+ * Called to notify that setup is complete.
+ *
+ * @param result The result of the setup process.
+ */
+ public void onIabSetupFinished(IabResult result);
+ }
+
+ /**
+ * Starts the setup process. This will start up the setup process asynchronously.
+ * You will be notified through the listener when the setup process is complete.
+ * This method is safe to call from a UI thread.
+ *
+ * @param listener The listener to notify when the setup process is complete.
+ */
+ public void startSetup(final OnIabSetupFinishedListener listener) {
+ // If already set up, can't do it again.
+ checkNotDisposed();
+ if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
+
+ // Connection to IAB service
+ logDebug("Starting in-app billing setup.");
+ mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ logDebug("Billing service disconnected.");
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (mDisposed) return;
+ logDebug("Billing service connected.");
+ mService = IInAppBillingService.Stub.asInterface(service);
+ String packageName = mContext.getPackageName();
+ try {
+ logDebug("Checking for in-app billing 3 support.");
+
+ // check for in-app billing v3 support
+ int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ if (listener != null) listener.onIabSetupFinished(new IabResult(response,
+ "Error checking for billing v3 support."));
+
+ // if in-app purchases aren't supported, neither are subscriptions.
+ mSubscriptionsSupported = false;
+ return;
+ }
+ logDebug("In-app billing version 3 supported for " + packageName);
+
+ // check for v3 subscriptions support
+ response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Subscriptions AVAILABLE.");
+ mSubscriptionsSupported = true;
+ }
+ else {
+ logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
+ }
+
+ mSetupDone = true;
+ }
+ catch (RemoteException e) {
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
+ "RemoteException while setting up in-app billing."));
+ }
+ e.printStackTrace();
+ return;
+ }
+
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
+ }
+ }
+ };
+
+ Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
+ serviceIntent.setPackage("com.android.vending");
+ if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
+ // service available to handle that Intent
+ mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+ }
+ else {
+ // no service available to handle that Intent
+ if (listener != null) {
+ listener.onIabSetupFinished(
+ new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
+ "Billing service unavailable on device."));
+ }
+ }
+ }
+
+ /**
+ * Dispose of object, releasing resources. It's very important to call this
+ * method when you are done with this object. It will release any resources
+ * used by it such as service connections. Naturally, once the object is
+ * disposed of, it can't be used again.
+ */
+ public void dispose() {
+ logDebug("Disposing.");
+ mSetupDone = false;
+ if (mServiceConn != null) {
+ logDebug("Unbinding from service.");
+ if (mContext != null) mContext.unbindService(mServiceConn);
+ }
+ mDisposed = true;
+ mContext = null;
+ mServiceConn = null;
+ mService = null;
+ mPurchaseListener = null;
+ }
+
+ private void checkNotDisposed() {
+ if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
+ }
+
+ /** Returns whether subscriptions are supported. */
+ public boolean subscriptionsSupported() {
+ checkNotDisposed();
+ return mSubscriptionsSupported;
+ }
+
+
+ /**
+ * Callback that notifies when a purchase is finished.
+ */
+ public interface OnIabPurchaseFinishedListener {
+ /**
+ * Called to notify that an in-app purchase finished. If the purchase was successful,
+ * then the sku parameter specifies which item was purchased. If the purchase failed,
+ * the sku and extraData parameters may or may not be null, depending on how far the purchase
+ * process went.
+ *
+ * @param result The result of the purchase.
+ * @param info The purchase information (null if purchase failed)
+ */
+ public void onIabPurchaseFinished(IabResult result, Purchase info);
+ }
+
+ // The listener registered on launchPurchaseFlow, which we have to call back when
+ // the purchase finishes
+ OnIabPurchaseFinishedListener mPurchaseListener;
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
+ launchPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener) {
+ launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
+ }
+
+ /**
+ * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
+ * which will involve bringing up the Google Play screen. The calling activity will be paused while
+ * the user interacts with Google Play, and the result will be delivered via the activity's
+ * {@link android.app.Activity#onActivityResult} method, at which point you must call
+ * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param act The calling activity.
+ * @param sku The sku of the item to purchase.
+ * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
+ * @param requestCode A request code (to differentiate from other responses --
+ * as in {@link android.app.Activity#startActivityForResult}).
+ * @param listener The listener to notify when the purchase process finishes
+ * @param extraData Extra data (developer payload), which will be returned with the purchase data
+ * when the purchase completes. This extra data will be permanently bound to that purchase
+ * and will always be returned when the purchase is queried.
+ */
+ public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ checkNotDisposed();
+ checkSetupDone("launchPurchaseFlow");
+ flagStartAsync("launchPurchaseFlow");
+ IabResult result;
+
+ if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
+ IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
+ "Subscriptions are not available.");
+ flagEndAsync();
+ if (listener != null) listener.onIabPurchaseFinished(r, null);
+ return;
+ }
+
+ try {
+ logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
+ Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
+ int response = getResponseCodeFromBundle(buyIntentBundle);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logError("Unable to buy item, Error response: " + getResponseDesc(response));
+ flagEndAsync();
+ result = new IabResult(response, "Unable to buy item");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ return;
+ }
+
+ PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
+ logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
+ mRequestCode = requestCode;
+ mPurchaseListener = listener;
+ mPurchasingItemType = itemType;
+ act.startIntentSenderForResult(pendingIntent.getIntentSender(),
+ requestCode, new Intent(),
+ Integer.valueOf(0), Integer.valueOf(0),
+ Integer.valueOf(0));
+ }
+ catch (SendIntentException e) {
+ logError("SendIntentException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ catch (RemoteException e) {
+ logError("RemoteException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ }
+
+ /**
+ * Handles an activity result that's part of the purchase flow in in-app billing. If you
+ * are calling {@link #launchPurchaseFlow}, then you must call this method from your
+ * Activity's {@link android.app.Activity@onActivityResult} method. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param requestCode The requestCode as you received it.
+ * @param resultCode The resultCode as you received it.
+ * @param data The data (Intent) as you received it.
+ * @return Returns true if the result was related to a purchase flow and was handled;
+ * false if the result was not related to a purchase, in which case you should
+ * handle it normally.
+ */
+ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+ IabResult result;
+ if (requestCode != mRequestCode) return false;
+
+ checkNotDisposed();
+ checkSetupDone("handleActivityResult");
+
+ // end of async purchase operation that started on launchPurchaseFlow
+ flagEndAsync();
+
+ if (data == null) {
+ logError("Null data in IAB activity result.");
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ int responseCode = getResponseCodeFromIntent(data);
+ String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
+ String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
+
+ if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successful resultcode from purchase activity.");
+ logDebug("Purchase data: " + purchaseData);
+ logDebug("Data signature: " + dataSignature);
+ logDebug("Extras: " + data.getExtras());
+ logDebug("Expected item type: " + mPurchasingItemType);
+
+ if (purchaseData == null || dataSignature == null) {
+ logError("BUG: either purchaseData or dataSignature is null.");
+ logDebug("Extras: " + data.getExtras().toString());
+ result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ Purchase purchase = null;
+ try {
+ purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
+ String sku = purchase.getSku();
+
+ // Verify signature
+ if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
+ logError("Purchase signature verification FAILED for sku " + sku);
+ result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
+ return true;
+ }
+ logDebug("Purchase signature successfully verified.");
+ }
+ catch (JSONException e) {
+ logError("Failed to parse purchase data.");
+ e.printStackTrace();
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ if (mPurchaseListener != null) {
+ mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
+ }
+ }
+ else if (resultCode == Activity.RESULT_OK) {
+ // result code was OK, but in-app billing response was not OK.
+ logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
+ if (mPurchaseListener != null) {
+ result = new IabResult(responseCode, "Problem purchashing item.");
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ }
+ else if (resultCode == Activity.RESULT_CANCELED) {
+ logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ else {
+ logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+ + ". Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ return true;
+ }
+
+ public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException {
+ return queryInventory(querySkuDetails, moreSkus, null);
+ }
+
+ /**
+ * Queries the inventory. This will query all owned items from the server, as well as
+ * information on additional skus, if specified. This method may block or take long to execute.
+ * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
+ *
+ * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
+ * as purchase information.
+ * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @throws IabException if a problem occurs while refreshing the inventory.
+ */
+ public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
+ List moreSubsSkus) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ try {
+ Inventory inv = new Inventory();
+ int r = queryPurchases(inv, ITEM_TYPE_INAPP);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned items).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of items).");
+ }
+ }
+
+ // if subscriptions are supported, then also query for subscriptions
+ if (mSubscriptionsSupported) {
+ r = queryPurchases(inv, ITEM_TYPE_SUBS);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
+ }
+ }
+ }
+
+ return inv;
+ }
+ catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
+ }
+ catch (JSONException e) {
+ throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
+ }
+ }
+
+ /**
+ * Listener that notifies when an inventory query operation completes.
+ */
+ public interface QueryInventoryFinishedListener {
+ /**
+ * Called to notify that an inventory query operation completed.
+ *
+ * @param result The result of the operation.
+ * @param inv The inventory.
+ */
+ public void onQueryInventoryFinished(IabResult result, Inventory inv);
+ }
+
+
+ /**
+ * Asynchronous wrapper for inventory query. This will perform an inventory
+ * query as described in {@link #queryInventory}, but will do so asynchronously
+ * and call back the specified listener upon completion. This method is safe to
+ * call from a UI thread.
+ *
+ * @param querySkuDetails as in {@link #queryInventory}
+ * @param moreSkus as in {@link #queryInventory}
+ * @param listener The listener to notify when the refresh operation completes.
+ */
+ public void queryInventoryAsync(final boolean querySkuDetails,
+ final List moreSkus,
+ final QueryInventoryFinishedListener listener) {
+ final Handler handler = new Handler();
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ flagStartAsync("refresh inventory");
+ (new Thread(new Runnable() {
+ public void run() {
+ IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
+ Inventory inv = null;
+ try {
+ inv = queryInventory(querySkuDetails, moreSkus);
+ }
+ catch (IabException ex) {
+ result = ex.getResult();
+ }
+
+ flagEndAsync();
+
+ final IabResult result_f = result;
+ final Inventory inv_f = inv;
+ if (!mDisposed && listener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ listener.onQueryInventoryFinished(result_f, inv_f);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(true, null, listener);
+ }
+
+ public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(querySkuDetails, null, listener);
+ }
+
+
+ /**
+ * Consumes a given in-app product. Consuming can only be done on an item
+ * that's owned, and as a result of consumption, the user will no longer own it.
+ * This method may block or take long to return. Do not call from the UI thread.
+ * For that, see {@link #consumeAsync}.
+ *
+ * @param itemInfo The PurchaseInfo that represents the item to consume.
+ * @throws IabException if there is a problem during consumption.
+ */
+ void consume(Purchase itemInfo) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("consume");
+
+ if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
+ throw new IabException(IABHELPER_INVALID_CONSUMPTION,
+ "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
+ }
+
+ try {
+ String token = itemInfo.getToken();
+ String sku = itemInfo.getSku();
+ if (token == null || token.equals("")) {
+ logError("Can't consume "+ sku + ". No token.");
+ throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ + sku + " " + itemInfo);
+ }
+
+ logDebug("Consuming sku: " + sku + ", token: " + token);
+ int response = mService.consumePurchase(3, mContext.getPackageName(), token);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successfully consumed sku: " + sku);
+ }
+ else {
+ logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
+ throw new IabException(response, "Error consuming sku " + sku);
+ }
+ }
+ catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
+ }
+ }
+
+ /**
+ * Callback that notifies when a consumption operation finishes.
+ */
+ public interface OnConsumeFinishedListener {
+ /**
+ * Called to notify that a consumption has finished.
+ *
+ * @param purchase The purchase that was (or was to be) consumed.
+ * @param result The result of the consumption operation.
+ */
+ public void onConsumeFinished(Purchase purchase, IabResult result);
+ }
+
+ /**
+ * Callback that notifies when a multi-item consumption operation finishes.
+ */
+ public interface OnConsumeMultiFinishedListener {
+ /**
+ * Called to notify that a consumption of multiple items has finished.
+ *
+ * @param purchases The purchases that were (or were to be) consumed.
+ * @param results The results of each consumption operation, corresponding to each
+ * sku.
+ */
+ public void onConsumeMultiFinished(List purchases, List results);
+ }
+
+ /**
+ * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
+ * performs the consumption in the background and notifies completion through
+ * the provided listener. This method is safe to call from a UI thread.
+ *
+ * @param purchase The purchase to be consumed.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ List purchases = new ArrayList();
+ purchases.add(purchase);
+ consumeAsyncInternal(purchases, listener, null);
+ }
+
+ /**
+ * Same as {@link consumeAsync}, but for multiple items at once.
+ * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ consumeAsyncInternal(purchases, null, listener);
+ }
+
+ /**
+ * Returns a human-readable description for the given response code.
+ *
+ * @param code The response code
+ * @return A human-readable string explaining the result code.
+ * It also includes the result code numerically.
+ */
+ public static String getResponseDesc(int code) {
+ String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
+ "3:Billing Unavailable/4:Item unavailable/" +
+ "5:Developer Error/6:Error/7:Item Already Owned/" +
+ "8:Item not owned").split("/");
+ String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
+ "-1002:Bad response received/" +
+ "-1003:Purchase signature verification failed/" +
+ "-1004:Send intent failed/" +
+ "-1005:User cancelled/" +
+ "-1006:Unknown purchase response/" +
+ "-1007:Missing token/" +
+ "-1008:Unknown error/" +
+ "-1009:Subscriptions not available/" +
+ "-1010:Invalid consumption attempt").split("/");
+
+ if (code <= IABHELPER_ERROR_BASE) {
+ int index = IABHELPER_ERROR_BASE - code;
+ if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
+ else return String.valueOf(code) + ":Unknown IAB Helper Error";
+ }
+ else if (code < 0 || code >= iab_msgs.length)
+ return String.valueOf(code) + ":Unknown";
+ else
+ return iab_msgs[code];
+ }
+
+
+ // Checks that setup was done; if not, throws an exception.
+ void checkSetupDone(String operation) {
+ if (!mSetupDone) {
+ logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
+ throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromBundle(Bundle b) {
+ Object o = b.get(RESPONSE_CODE);
+ if (o == null) {
+ logDebug("Bundle with null response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+ else if (o instanceof Integer) return ((Integer)o).intValue();
+ else if (o instanceof Long) return (int)((Long)o).longValue();
+ else {
+ logError("Unexpected type for bundle response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromIntent(Intent i) {
+ Object o = i.getExtras().get(RESPONSE_CODE);
+ if (o == null) {
+ logError("Intent with no response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+ else if (o instanceof Integer) return ((Integer)o).intValue();
+ else if (o instanceof Long) return (int)((Long)o).longValue();
+ else {
+ logError("Unexpected type for intent response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
+ }
+ }
+
+ void flagStartAsync(String operation) {
+ if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
+ operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
+ mAsyncOperation = operation;
+ mAsyncInProgress = true;
+ logDebug("Starting async operation: " + operation);
+ }
+
+ void flagEndAsync() {
+ logDebug("Ending async operation: " + mAsyncOperation);
+ mAsyncOperation = "";
+ mAsyncInProgress = false;
+ }
+
+
+ int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
+ // Query purchases
+ logDebug("Querying owned items, item type: " + itemType);
+ logDebug("Package name: " + mContext.getPackageName());
+ boolean verificationFailed = false;
+ String continueToken = null;
+
+ do {
+ logDebug("Calling getPurchases with continuation token: " + continueToken);
+ Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
+ itemType, continueToken);
+
+ int response = getResponseCodeFromBundle(ownedItems);
+ logDebug("Owned items response: " + String.valueOf(response));
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getPurchases() failed: " + getResponseDesc(response));
+ return response;
+ }
+ if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
+ logError("Bundle returned from getPurchases() doesn't contain required fields.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+
+ ArrayList ownedSkus = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_ITEM_LIST);
+ ArrayList purchaseDataList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_PURCHASE_DATA_LIST);
+ ArrayList signatureList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_SIGNATURE_LIST);
+
+ for (int i = 0; i < purchaseDataList.size(); ++i) {
+ String purchaseData = purchaseDataList.get(i);
+ String signature = signatureList.get(i);
+ String sku = ownedSkus.get(i);
+ if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
+ logDebug("Sku is owned: " + sku);
+ Purchase purchase = new Purchase(itemType, purchaseData, signature);
+
+ if (TextUtils.isEmpty(purchase.getToken())) {
+ logWarn("BUG: empty/null token!");
+ logDebug("Purchase data: " + purchaseData);
+ }
+
+ // Record ownership and token
+ inv.addPurchase(purchase);
+ }
+ else {
+ logWarn("Purchase signature verification **FAILED**. Not adding item.");
+ logDebug(" Purchase data: " + purchaseData);
+ logDebug(" Signature: " + signature);
+ verificationFailed = true;
+ }
+ }
+
+ continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
+ logDebug("Continuation token: " + continueToken);
+ } while (!TextUtils.isEmpty(continueToken));
+
+ return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
+ }
+
+ int querySkuDetails(String itemType, Inventory inv, List moreSkus)
+ throws RemoteException, JSONException {
+ logDebug("Querying SKU details.");
+ ArrayList skuList = new ArrayList();
+ skuList.addAll(inv.getAllOwnedSkus(itemType));
+ if (moreSkus != null) {
+ for (String sku : moreSkus) {
+ if (!skuList.contains(sku)) {
+ skuList.add(sku);
+ }
+ }
+ }
+
+ if (skuList.size() == 0) {
+ logDebug("queryPrices: nothing to do because there are no SKUs.");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
+ Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
+ itemType, querySkus);
+
+ if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
+ int response = getResponseCodeFromBundle(skuDetails);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getSkuDetails() failed: " + getResponseDesc(response));
+ return response;
+ }
+ else {
+ logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+ }
+
+ ArrayList responseList = skuDetails.getStringArrayList(
+ RESPONSE_GET_SKU_DETAILS_LIST);
+
+ for (String thisResponse : responseList) {
+ SkuDetails d = new SkuDetails(itemType, thisResponse);
+ logDebug("Got sku details: " + d);
+ inv.addSkuDetails(d);
+ }
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+
+ void consumeAsyncInternal(final List purchases,
+ final OnConsumeFinishedListener singleListener,
+ final OnConsumeMultiFinishedListener multiListener) {
+ final Handler handler = new Handler();
+ flagStartAsync("consume");
+ (new Thread(new Runnable() {
+ public void run() {
+ final List results = new ArrayList();
+ for (Purchase purchase : purchases) {
+ try {
+ consume(purchase);
+ results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
+ }
+ catch (IabException ex) {
+ results.add(ex.getResult());
+ }
+ }
+
+ flagEndAsync();
+ if (!mDisposed && singleListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ singleListener.onConsumeFinished(purchases.get(0), results.get(0));
+ }
+ });
+ }
+ if (!mDisposed && multiListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ multiListener.onConsumeMultiFinished(purchases, results);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ void logDebug(String msg) {
+ if (mDebugLog) Log.d(mDebugTag, msg);
+ }
+
+ void logError(String msg) {
+ Log.e(mDebugTag, "In-app billing error: " + msg);
+ }
+
+ void logWarn(String msg) {
+ Log.w(mDebugTag, "In-app billing warning: " + msg);
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabResult.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabResult.java
new file mode 100644
index 0000000..8a20158
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/IabResult.java
@@ -0,0 +1,45 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.survivingwithandroid.weather.billing.util;
+
+/**
+ * Represents the result of an in-app billing operation.
+ * A result is composed of a response code (an integer) and possibly a
+ * message (String). You can get those by calling
+ * {@link #getResponse} and {@link #getMessage()}, respectively. You
+ * can also inquire whether a result is a success or a failure by
+ * calling {@link #isSuccess()} and {@link #isFailure()}.
+ */
+public class IabResult {
+ int mResponse;
+ String mMessage;
+
+ public IabResult(int response, String message) {
+ mResponse = response;
+ if (message == null || message.trim().length() == 0) {
+ mMessage = IabHelper.getResponseDesc(response);
+ }
+ else {
+ mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
+ }
+ }
+ public int getResponse() { return mResponse; }
+ public String getMessage() { return mMessage; }
+ public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
+ public boolean isFailure() { return !isSuccess(); }
+ public String toString() { return "IabResult: " + getMessage(); }
+}
+
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Inventory.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Inventory.java
new file mode 100644
index 0000000..28969f0
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Inventory.java
@@ -0,0 +1,91 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.survivingwithandroid.weather.billing.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a block of information about in-app items.
+ * An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
+ */
+public class Inventory {
+ Map mSkuMap = new HashMap();
+ Map mPurchaseMap = new HashMap();
+
+ Inventory() { }
+
+ /** Returns the listing details for an in-app product. */
+ public SkuDetails getSkuDetails(String sku) {
+ return mSkuMap.get(sku);
+ }
+
+ /** Returns purchase information for a given product, or null if there is no purchase. */
+ public Purchase getPurchase(String sku) {
+ return mPurchaseMap.get(sku);
+ }
+
+ /** Returns whether or not there exists a purchase of the given product. */
+ public boolean hasPurchase(String sku) {
+ return mPurchaseMap.containsKey(sku);
+ }
+
+ /** Return whether or not details about the given product are available. */
+ public boolean hasDetails(String sku) {
+ return mSkuMap.containsKey(sku);
+ }
+
+ /**
+ * Erase a purchase (locally) from the inventory, given its product ID. This just
+ * modifies the Inventory object locally and has no effect on the server! This is
+ * useful when you have an existing Inventory object which you know to be up to date,
+ * and you have just consumed an item successfully, which means that erasing its
+ * purchase data from the Inventory you already have is quicker than querying for
+ * a new Inventory.
+ */
+ public void erasePurchase(String sku) {
+ if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
+ }
+
+ /** Returns a list of all owned product IDs. */
+ List getAllOwnedSkus() {
+ return new ArrayList(mPurchaseMap.keySet());
+ }
+
+ /** Returns a list of all owned product IDs of a given type */
+ List getAllOwnedSkus(String itemType) {
+ List result = new ArrayList();
+ for (Purchase p : mPurchaseMap.values()) {
+ if (p.getItemType().equals(itemType)) result.add(p.getSku());
+ }
+ return result;
+ }
+
+ /** Returns a list of all purchases. */
+ List getAllPurchases() {
+ return new ArrayList(mPurchaseMap.values());
+ }
+
+ void addSkuDetails(SkuDetails d) {
+ mSkuMap.put(d.getSku(), d);
+ }
+
+ void addPurchase(Purchase p) {
+ mPurchaseMap.put(p.getSku(), p);
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Purchase.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Purchase.java
new file mode 100644
index 0000000..e915340
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Purchase.java
@@ -0,0 +1,63 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.survivingwithandroid.weather.billing.util;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app billing purchase.
+ */
+public class Purchase {
+ String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
+ String mOrderId;
+ String mPackageName;
+ String mSku;
+ long mPurchaseTime;
+ int mPurchaseState;
+ String mDeveloperPayload;
+ String mToken;
+ String mOriginalJson;
+ String mSignature;
+
+ public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
+ mItemType = itemType;
+ mOriginalJson = jsonPurchaseInfo;
+ JSONObject o = new JSONObject(mOriginalJson);
+ mOrderId = o.optString("orderId");
+ mPackageName = o.optString("packageName");
+ mSku = o.optString("productId");
+ mPurchaseTime = o.optLong("purchaseTime");
+ mPurchaseState = o.optInt("purchaseState");
+ mDeveloperPayload = o.optString("developerPayload");
+ mToken = o.optString("token", o.optString("purchaseToken"));
+ mSignature = signature;
+ }
+
+ public String getItemType() { return mItemType; }
+ public String getOrderId() { return mOrderId; }
+ public String getPackageName() { return mPackageName; }
+ public String getSku() { return mSku; }
+ public long getPurchaseTime() { return mPurchaseTime; }
+ public int getPurchaseState() { return mPurchaseState; }
+ public String getDeveloperPayload() { return mDeveloperPayload; }
+ public String getToken() { return mToken; }
+ public String getOriginalJson() { return mOriginalJson; }
+ public String getSignature() { return mSignature; }
+
+ @Override
+ public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Security.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Security.java
new file mode 100644
index 0000000..f0ef526
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/Security.java
@@ -0,0 +1,123 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.survivingwithandroid.weather.billing.util;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * Security-related methods. For a secure implementation, all of this code
+ * should be implemented on a server that communicates with the
+ * application on the device. For the sake of simplicity and clarity of this
+ * example, this code is included here and is executed on the device. If you
+ * must verify the purchases on the phone, you should obfuscate this code to
+ * make it harder for an attacker to replace the code with stubs that treat all
+ * purchases as verified.
+ */
+public class Security {
+ private static final String TAG = "IABUtil/Security";
+
+ private static final String KEY_FACTORY_ALGORITHM = "RSA";
+ private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+ /**
+ * Verifies that the data was signed with the given signature, and returns
+ * the verified purchase. The data is in JSON format and signed
+ * with a private key. The data also contains the {@link PurchaseState}
+ * and product ID of the purchase.
+ * @param base64PublicKey the base64-encoded public key to use for verifying.
+ * @param signedData the signed JSON string (signed, not encrypted)
+ * @param signature the signature for the data, signed with the private key
+ */
+ public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
+ if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
+ TextUtils.isEmpty(signature)) {
+ Log.e(TAG, "Purchase verification failed: missing data.");
+ return false;
+ }
+
+ PublicKey key = Security.generatePublicKey(base64PublicKey);
+ return Security.verify(key, signedData, signature);
+ }
+
+ /**
+ * Generates a PublicKey instance from a string containing the
+ * Base64-encoded public key.
+ *
+ * @param encodedPublicKey Base64-encoded public key
+ * @throws IllegalArgumentException if encodedPublicKey is invalid
+ */
+ public static PublicKey generatePublicKey(String encodedPublicKey) {
+ try {
+ byte[] decodedKey = Base64.decode(encodedPublicKey);
+ KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+ return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ Log.e(TAG, "Invalid key specification.");
+ throw new IllegalArgumentException(e);
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Verifies that the signature from the server matches the computed
+ * signature on the data. Returns true if the data is correctly signed.
+ *
+ * @param publicKey public key associated with the developer account
+ * @param signedData signed data from server
+ * @param signature server signature
+ * @return true if the data and signature match
+ */
+ public static boolean verify(PublicKey publicKey, String signedData, String signature) {
+ Signature sig;
+ try {
+ sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+ sig.initVerify(publicKey);
+ sig.update(signedData.getBytes());
+ if (!sig.verify(Base64.decode(signature))) {
+ Log.e(TAG, "Signature verification failed.");
+ return false;
+ }
+ return true;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "NoSuchAlgorithmException.");
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Invalid key specification.");
+ } catch (SignatureException e) {
+ Log.e(TAG, "Signature exception.");
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ }
+ return false;
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/SkuDetails.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/SkuDetails.java
new file mode 100644
index 0000000..266c19e
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/SkuDetails.java
@@ -0,0 +1,58 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.survivingwithandroid.weather.billing.util;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app product's listing details.
+ */
+public class SkuDetails {
+ String mItemType;
+ String mSku;
+ String mType;
+ String mPrice;
+ String mTitle;
+ String mDescription;
+ String mJson;
+
+ public SkuDetails(String jsonSkuDetails) throws JSONException {
+ this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
+ }
+
+ public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
+ mItemType = itemType;
+ mJson = jsonSkuDetails;
+ JSONObject o = new JSONObject(mJson);
+ mSku = o.optString("productId");
+ mType = o.optString("type");
+ mPrice = o.optString("price");
+ mTitle = o.optString("title");
+ mDescription = o.optString("description");
+ }
+
+ public String getSku() { return mSku; }
+ public String getType() { return mType; }
+ public String getPrice() { return mPrice; }
+ public String getTitle() { return mTitle; }
+ public String getDescription() { return mDescription; }
+
+ @Override
+ public String toString() {
+ return "SkuDetails:" + mJson;
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/SwABillingUtil.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/SwABillingUtil.java
new file mode 100644
index 0000000..447c8f4
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/billing/util/SwABillingUtil.java
@@ -0,0 +1,73 @@
+package com.survivingwithandroid.weather.billing.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.PopupWindow;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+
+import com.survivingwithandroid.weather.R;
+
+/**
+ * Created by Francesco on 21/02/14.
+ */
+public class SwABillingUtil {
+
+ public static String KEY = "Your_key_here";
+ private static String SKU_DONATE_W2 = "SKU_IN_APP";
+ //private static String SKU_DONATE_W2 = "android.test.purchased";
+
+
+ public static void showDonateDialog(final Context ctx, final IabHelper mHelper, final Activity act) {
+ LayoutInflater inf = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View v = inf.inflate(R.layout.popdonate_layout, null, false);
+
+ final PopupWindow pw = new PopupWindow(v);
+ pw.setWidth(RelativeLayout.LayoutParams.WRAP_CONTENT);
+ pw.setHeight(RelativeLayout.LayoutParams.WRAP_CONTENT);
+
+ Button canBtn = (Button) v.findViewById(R.id.btnCancelDonate);
+
+
+ canBtn.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ pw.dismiss();
+ }
+ });
+
+ final Button donBtn = (Button) v.findViewById(R.id.btnDonate);
+ donBtn.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ String payload="test";
+
+ mHelper.launchPurchaseFlow(act, SKU_DONATE_W2, 2000,
+ new IabHelper.OnIabPurchaseFinishedListener() {
+ @Override
+ public void onIabPurchaseFinished(IabResult result, Purchase info) {
+ if (result.isFailure()) {
+ Log.i("SwA", "Failure = " + result);
+ //Toast.makeText(getActivity(), "Error purchasing: " + result, Toast.LENGTH_LONG).show();
+ return;
+ } else if (info.getSku().equals(SKU_DONATE_W2)) {
+ donBtn.setClickable(false);
+ Toast.makeText(ctx, "Thank you!", Toast.LENGTH_LONG).show();
+ }
+ }
+ }, payload);
+ pw.dismiss();
+
+ }
+ });
+
+ pw.showAtLocation(v, Gravity.CENTER, 0, 0);
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/model/CityResult.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/model/CityResult.java
new file mode 100644
index 0000000..8ec9f77
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/model/CityResult.java
@@ -0,0 +1,60 @@
+package com.survivingwithandroid.weather.model;
+
+/*
+ * Copyright (C) 2014 Francesco Azzola - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class CityResult {
+
+ private String woeid;
+ private String cityName;
+ private String country;
+
+ public CityResult() {}
+
+ public CityResult(String woeid, String cityName, String country) {
+ this.woeid = woeid;
+ this.cityName = cityName;
+ this.country = country;
+ }
+
+ public String getWoeid() {
+ return woeid;
+ }
+
+ public void setWoeid(String woeid) {
+ this.woeid = woeid;
+ }
+
+ public String getCityName() {
+ return cityName;
+ }
+
+ public void setCityName(String cityName) {
+ this.cityName = cityName;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ @Override
+ public String toString() {
+ return cityName + "," + country;
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/model/Weather.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/model/Weather.java
new file mode 100644
index 0000000..f7beaa6
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/model/Weather.java
@@ -0,0 +1,76 @@
+package com.survivingwithandroid.weather.model;
+/*
+ * Copyright (C) 2014 Francesco Azzola - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class Weather {
+
+ public String imageUrl;
+
+ public Condition condition = new Condition();
+ public Wind wind = new Wind();
+ public Atmosphere atmosphere = new Atmosphere();
+ public Forecast forecast = new Forecast();
+ public Location location = new Location();
+ public Astronomy astronomy = new Astronomy();
+ public Units units = new Units();
+
+ public String lastUpdate;
+
+ public class Condition {
+ public String description;
+ public int code;
+ public String date;
+ public int temp;
+ }
+
+ public class Forecast {
+ public int tempMin;
+ public int tempMax;
+ public String description;
+ public int code;
+ }
+
+ public static class Atmosphere {
+ public int humidity;
+ public float visibility;
+ public float pressure;
+ public int rising;
+ }
+
+ public class Wind {
+ public int chill;
+ public int direction;
+ public int speed;
+ }
+
+ public class Units {
+ public String speed;
+ public String distance;
+ public String pressure;
+ public String temperature;
+ }
+
+ public class Location {
+ public String name;
+ public String region;
+ public String country;
+ }
+
+ public class Astronomy {
+ public String sunRise;
+ public String sunSet;
+ }
+
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/IWeatherImageProvider.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/IWeatherImageProvider.java
new file mode 100644
index 0000000..4e0d922
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/IWeatherImageProvider.java
@@ -0,0 +1,30 @@
+package com.survivingwithandroid.weather.provider;
+
+import android.graphics.Bitmap;
+
+import com.android.volley.RequestQueue;
+
+/*
+ * Copyright (C) 2014 (Francesco Azzola) - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public interface IWeatherImageProvider {
+
+ public Bitmap getImage(int code,RequestQueue requestQueue, WeatherImageListener listener);
+
+
+ public static interface WeatherImageListener {
+ public void onImageReady(Bitmap image);
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/WeatherImageProvider.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/WeatherImageProvider.java
new file mode 100644
index 0000000..851215b
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/WeatherImageProvider.java
@@ -0,0 +1,45 @@
+package com.survivingwithandroid.weather.provider;
+
+
+import android.graphics.Bitmap;
+
+import com.android.volley.RequestQueue;
+import com.android.volley.Response;
+import com.android.volley.toolbox.ImageRequest;
+
+/*
+ * Copyright (C) 2014 Francesco Azzola - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ // We retrieve the image from YAHOO! Weather, but you can use any kind of image provider you like
+
+public class WeatherImageProvider implements IWeatherImageProvider {
+ private static final String YAHOO_IMG_URL = "http://l.yimg.com/a/i/us/we/52/";
+
+ @Override
+ public Bitmap getImage(int code, RequestQueue requestQueue, final WeatherImageListener listener) {
+ String imageURL = YAHOO_IMG_URL + code + ".gif";
+ ImageRequest ir = new ImageRequest(imageURL, new Response.Listener() {
+
+ @Override
+ public void onResponse(Bitmap response) {
+ if (listener != null)
+ listener.onImageReady(response);
+ }
+ }, 0, 0, null, null);
+
+ requestQueue.add(ir);
+ return null;
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/YahooClient.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/YahooClient.java
new file mode 100644
index 0000000..281dec6
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/provider/YahooClient.java
@@ -0,0 +1,239 @@
+package com.survivingwithandroid.weather.provider;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.toolbox.Volley;
+import com.survivingwithandroid.weather.Config;
+import com.survivingwithandroid.weather.MainActivity;
+import com.survivingwithandroid.weather.model.CityResult;
+import com.survivingwithandroid.weather.model.Weather;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+ * Copyright (C) 2014 Francesco Azzola - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class YahooClient {
+ public static String YAHOO_GEO_URL = "http://where.yahooapis.com/v1";
+ public static String YAHOO_WEATHER_URL = "http://weather.yahooapis.com/forecastrss";
+
+ private static String APPID = "APP_ID_KEY";
+
+ public static List getCityList(String cityName) {
+ List result = new ArrayList();
+ HttpURLConnection yahooHttpConn = null;
+ try {
+ String query =makeQueryCityURL(cityName);
+ //Log.d("Swa", "URL [" + query + "]");
+ yahooHttpConn= (HttpURLConnection) (new URL(query)).openConnection();
+ yahooHttpConn.connect();
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(new InputStreamReader(yahooHttpConn.getInputStream()));
+ Log.d("Swa", "XML Parser ok");
+ int event = parser.getEventType();
+
+ CityResult cty = null;
+ String tagName = null;
+ String currentTag = null;
+
+ // We start parsing the XML
+ while (event != XmlPullParser.END_DOCUMENT) {
+ tagName = parser.getName();
+
+ if (event == XmlPullParser.START_TAG) {
+ if (tagName.equals("place")) {
+ // place Tag Found so we create a new CityResult
+ cty = new CityResult();
+ // Log.d("Swa", "New City found");
+ }
+ currentTag = tagName;
+ // Log.d("Swa", "Tag ["+tagName+"]");
+ }
+ else if (event == XmlPullParser.TEXT) {
+ // We found some text. let's see the tagName to know the tag related to the text
+ if ("woeid".equals(currentTag))
+ cty.setWoeid(parser.getText());
+ else if ("name".equals(currentTag))
+ cty.setCityName(parser.getText());
+ else if ("country".equals(currentTag))
+ cty.setCountry(parser.getText());
+
+ // We don't want to analyze other tag at the moment
+ }
+ else if (event == XmlPullParser.END_TAG) {
+ if ("place".equals(tagName))
+ result.add(cty);
+ }
+
+ event = parser.next();
+ }
+ }
+ catch(Throwable t) {
+ t.printStackTrace();
+ // Log.e("Error in getCityList", t.getMessage());
+ }
+ finally {
+ try {
+ yahooHttpConn.disconnect();
+ }
+ catch(Throwable ignore) {}
+
+ }
+ return result;
+ }
+
+ public static void getWeather(String woeid, String unit, RequestQueue rq, final WeatherClientListener listener) {
+ String url2Call = makeWeatherURL(woeid, unit);
+ // Log.d("SwA", "Weather URL ["+url2Call+"]");
+ final Weather result = new Weather();
+ StringRequest req = new StringRequest(Request.Method.GET, url2Call, new Response.Listener() {
+ @Override
+ public void onResponse(String s) {
+ parseResponse(s, result);
+ listener.onWeatherResponse(result);
+ }
+ }, new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+
+ }
+ });
+
+ rq.add(req);
+ }
+
+ private static Weather parseResponse (String resp, Weather result) {
+ // Log.d("SwA", "Response ["+resp+"]");
+ try {
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(new StringReader(resp));
+
+ String tagName = null;
+ String currentTag = null;
+
+ int event = parser.getEventType();
+ boolean isFirstDayForecast = true;
+ while (event != XmlPullParser.END_DOCUMENT) {
+ tagName = parser.getName();
+
+ if (event == XmlPullParser.START_TAG) {
+ if (tagName.equals("yweather:wind")) {
+ // Log.d("SwA", "Tag [Wind]");
+ result.wind.chill = Integer.parseInt(parser.getAttributeValue(null, "chill"));
+ result.wind.direction = Integer.parseInt(parser.getAttributeValue(null, "direction"));
+ result.wind.speed = (int) Float.parseFloat(parser.getAttributeValue(null, "speed"));
+ }
+ else if (tagName.equals("yweather:atmosphere")) {
+ // Log.d("SwA", "Tag [Atmos]");
+ result.atmosphere.humidity = Integer.parseInt(parser.getAttributeValue(null, "humidity"));
+ result.atmosphere.visibility = Float.parseFloat(parser.getAttributeValue(null, "visibility"));
+ result.atmosphere.pressure = Float.parseFloat(parser.getAttributeValue(null, "pressure"));
+ result.atmosphere.rising = Integer.parseInt(parser.getAttributeValue(null, "rising"));
+ }
+ else if (tagName.equals("yweather:forecast")) {
+ // Log.d("SwA", "Tag [Fore]");
+ if (isFirstDayForecast) {
+ result.forecast.code = Integer.parseInt(parser.getAttributeValue(null, "code"));
+ result.forecast.tempMin = Integer.parseInt(parser.getAttributeValue(null, "low"));
+ result.forecast.tempMax = Integer.parseInt(parser.getAttributeValue(null, "high"));
+ isFirstDayForecast = false;
+ }
+ }
+ else if (tagName.equals("yweather:condition")) {
+ // Log.d("SwA", "Tag [Condition]");
+ result.condition.code = Integer.parseInt(parser.getAttributeValue(null, "code"));
+ result.condition.description = parser.getAttributeValue(null, "text");
+ result.condition.temp = Integer.parseInt(parser.getAttributeValue(null, "temp"));
+ result.condition.date = parser.getAttributeValue(null, "date");
+ }
+ else if (tagName.equals("yweather:units")) {
+ // Log.d("SwA", "Tag [units]");
+ result.units.temperature = "°" + parser.getAttributeValue(null, "temperature");
+ result.units.pressure = parser.getAttributeValue(null, "pressure");
+ result.units.distance = parser.getAttributeValue(null, "distance");
+ result.units.speed = parser.getAttributeValue(null, "speed");
+ }
+ else if (tagName.equals("yweather:location")) {
+ result.location.name = parser.getAttributeValue(null, "city");
+ result.location.region = parser.getAttributeValue(null, "region");
+ result.location.country = parser.getAttributeValue(null, "country");
+ }
+ else if (tagName.equals("image"))
+ currentTag = "image";
+ else if (tagName.equals("url")) {
+ if (currentTag == null) {
+ result.imageUrl = parser.getAttributeValue(null, "src");
+ }
+ }
+ else if (tagName.equals("lastBuildDate")) {
+ currentTag="update";
+ }
+ else if (tagName.equals("yweather:astronomy")) {
+ result.astronomy.sunRise = parser.getAttributeValue(null, "sunrise");
+ result.astronomy.sunSet = parser.getAttributeValue(null, "sunset");
+ }
+
+ }
+ else if (event == XmlPullParser.END_TAG) {
+ if ("image".equals(currentTag)) {
+ currentTag = null;
+ }
+ }
+ else if (event == XmlPullParser.TEXT) {
+ if ("update".equals(currentTag))
+ result.lastUpdate = parser.getText();
+ }
+ event = parser.next();
+ }
+
+ }
+ catch(Throwable t) {
+ t.printStackTrace();
+ }
+
+ return result;
+ }
+
+ private static String makeQueryCityURL(String cityName) {
+ // We remove spaces in cityName
+ cityName = cityName.replaceAll(" ", "%20");
+ return YAHOO_GEO_URL + "/places.q(" + cityName + "%2A);count=" + Config.MAX_CITY_RESULT + "?appid=" + APPID;
+ }
+
+ private static String makeWeatherURL(String woeid, String unit) {
+ return YAHOO_WEATHER_URL + "?w=" + woeid + "&u=" + unit;
+ }
+
+
+ /* Pubblic listener interface */
+
+ public static interface WeatherClientListener {
+ public void onWeatherResponse(Weather weather);
+ }
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/settings/CityFinderActivity.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/settings/CityFinderActivity.java
new file mode 100644
index 0000000..dd27af7
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/settings/CityFinderActivity.java
@@ -0,0 +1,152 @@
+package com.survivingwithandroid.weather.settings;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NavUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.TextView;
+
+import com.survivingwithandroid.weather.R;
+import com.survivingwithandroid.weather.model.CityResult;
+import com.survivingwithandroid.weather.provider.YahooClient;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+ * Copyright (C) 2014 Francesco Azzola - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class CityFinderActivity extends Activity {
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.cityfinder_layout);
+ AutoCompleteTextView edt = (AutoCompleteTextView) this.findViewById(R.id.edtCity);
+ CityAdapter adpt = new CityAdapter(this, null);
+ edt.setAdapter(adpt);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ edt.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ CityResult result = (CityResult) parent.getItemAtPosition(position);
+ SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(CityFinderActivity.this);
+ //Log.d("SwA", "WOEID [" + result.getWoeid() + "]");
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString("woeid", result.getWoeid());
+ editor.putString("cityName", result.getCityName());
+ editor.putString("country", result.getCountry());
+ editor.commit();
+ NavUtils.navigateUpFromSameTask(CityFinderActivity.this);
+ }
+ });
+
+ }
+
+ private class CityAdapter extends ArrayAdapter implements Filterable {
+
+ private Context ctx;
+ private List cityList = new ArrayList();
+
+ public CityAdapter(Context ctx, List cityList) {
+ super(ctx, R.layout.cityresult_layout, cityList);
+ this.cityList = cityList;
+ this.ctx = ctx;
+ }
+
+
+ @Override
+ public CityResult getItem(int position) {
+ if (cityList != null)
+ return cityList.get(position);
+
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ if (cityList != null)
+ return cityList.size();
+
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View result = convertView;
+
+ if (result == null) {
+ LayoutInflater inf = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ result = inf.inflate(R.layout.cityresult_layout, parent, false);
+
+ }
+
+ TextView tv = (TextView) result.findViewById(R.id.txtCityName);
+ tv.setText(cityList.get(position).getCityName() + "," + cityList.get(position).getCountry());
+
+ return result;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (cityList != null)
+ return cityList.get(position).hashCode();
+
+ return 0;
+ }
+
+ @Override
+ public Filter getFilter() {
+ Filter cityFilter = new Filter() {
+
+
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ FilterResults results = new FilterResults();
+ if (constraint == null || constraint.length() < 2)
+ return results;
+
+ List cityResultList = YahooClient.getCityList(constraint.toString());
+ results.values = cityResultList;
+ results.count = cityResultList.size();
+ return results;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ cityList = (List) results.values;
+ notifyDataSetChanged();
+ }
+ };
+
+ return cityFilter;
+ }
+ }
+
+
+}
diff --git a/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/settings/WeatherPreferenceActivity.java b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/settings/WeatherPreferenceActivity.java
new file mode 100644
index 0000000..5c9bc6f
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/java/com/survivingwithandroid/weather/settings/WeatherPreferenceActivity.java
@@ -0,0 +1,61 @@
+package com.survivingwithandroid.weather.settings;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.survivingwithandroid.weather.R;
+import com.survivingwithandroid.weather.model.CityResult;
+
+import java.util.List;
+/*
+ * Copyright (C) 2014 Francesco Azzola - Surviving with Android (http://www.survivingwithandroid.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class WeatherPreferenceActivity extends PreferenceActivity {
+
+
+ @Override
+ public void onCreate(Bundle Bundle) {
+ super.onCreate(Bundle);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ String action = getIntent().getAction();
+
+ addPreferencesFromResource(R.xml.weather_prefs);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ // We set the current values in the description
+ Preference prefLocation = getPreferenceScreen().findPreference("swa_loc");
+ Preference prefTemp = getPreferenceScreen().findPreference("swa_temp_unit");
+
+ prefLocation.setSummary(getResources().getText(R.string.summary_loc) + " " + prefs.getString("cityName", null) + "," + prefs.getString("country", null));
+
+ String unit = prefs.getString("swa_temp_unit", null) != null ? "°" + prefs.getString("swa_temp_unit", null).toUpperCase() : "";
+ prefTemp.setSummary(getResources().getText(R.string.summary_temp) + " " + unit);
+
+
+ //getPreferenceScreen().findPreference("swa_temp_unit").setDefaultValue(valTemp);
+ //getPreferenceScreen().findPreference("swa_system").setDefaultValue(unitSys);
+
+
+ }
+
+
+
+
+
+
+}
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable-hdpi/ic_launcher.png b/AndroidYahooWeather/weather/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..c1b44cc
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable-mdpi/ic_launcher.png b/AndroidYahooWeather/weather/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..a1bf709
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable-mdpi/ic_launcher.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable-xhdpi/ic_launcher.png b/AndroidYahooWeather/weather/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..3b5ae5b
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable-xxhdpi/ic_launcher.png b/AndroidYahooWeather/weather/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b7df51b
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/eye.png b/AndroidYahooWeather/weather/src/main/res/drawable/eye.png
new file mode 100644
index 0000000..69e1b9c
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable/eye.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/humidity.png b/AndroidYahooWeather/weather/src/main/res/drawable/humidity.png
new file mode 100644
index 0000000..ae404d4
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable/humidity.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/ic_menu_refresh.png b/AndroidYahooWeather/weather/src/main/res/drawable/ic_menu_refresh.png
new file mode 100644
index 0000000..e13315f
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable/ic_menu_refresh.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_blue.xml b/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_blue.xml
new file mode 100644
index 0000000..0543949
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_blue.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_green.xml b/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_green.xml
new file mode 100644
index 0000000..b2357a9
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_green.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_red.xml b/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_red.xml
new file mode 100644
index 0000000..b0e2db6
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/drawable/line_shape_red.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/moon.png b/AndroidYahooWeather/weather/src/main/res/drawable/moon.png
new file mode 100644
index 0000000..640a4e7
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable/moon.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/pressure.png b/AndroidYahooWeather/weather/src/main/res/drawable/pressure.png
new file mode 100644
index 0000000..fa16d97
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable/pressure.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/sun.png b/AndroidYahooWeather/weather/src/main/res/drawable/sun.png
new file mode 100644
index 0000000..495e0c5
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable/sun.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/temperature.png b/AndroidYahooWeather/weather/src/main/res/drawable/temperature.png
new file mode 100644
index 0000000..9bf9453
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable/temperature.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/drawable/wind.png b/AndroidYahooWeather/weather/src/main/res/drawable/wind.png
new file mode 100644
index 0000000..da56fd7
Binary files /dev/null and b/AndroidYahooWeather/weather/src/main/res/drawable/wind.png differ
diff --git a/AndroidYahooWeather/weather/src/main/res/layout-land/fragment_main.xml b/AndroidYahooWeather/weather/src/main/res/layout-land/fragment_main.xml
new file mode 100644
index 0000000..4e3f78d
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/layout-land/fragment_main.xml
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/layout/activity_main.xml b/AndroidYahooWeather/weather/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..3899cab
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/layout/activity_main.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/layout/cityfinder_layout.xml b/AndroidYahooWeather/weather/src/main/res/layout/cityfinder_layout.xml
new file mode 100644
index 0000000..4b3b7b8
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/layout/cityfinder_layout.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/layout/cityresult_layout.xml b/AndroidYahooWeather/weather/src/main/res/layout/cityresult_layout.xml
new file mode 100644
index 0000000..2a40909
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/layout/cityresult_layout.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AndroidYahooWeather/weather/src/main/res/layout/fragment_main.xml b/AndroidYahooWeather/weather/src/main/res/layout/fragment_main.xml
new file mode 100644
index 0000000..7a4cad9
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/layout/fragment_main.xml
@@ -0,0 +1,254 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/layout/popdonate_layout.xml b/AndroidYahooWeather/weather/src/main/res/layout/popdonate_layout.xml
new file mode 100644
index 0000000..049b405
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/layout/popdonate_layout.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/layout/progress_bar.xml b/AndroidYahooWeather/weather/src/main/res/layout/progress_bar.xml
new file mode 100644
index 0000000..5b73785
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/layout/progress_bar.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AndroidYahooWeather/weather/src/main/res/menu/main.xml b/AndroidYahooWeather/weather/src/main/res/menu/main.xml
new file mode 100644
index 0000000..493b22b
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/menu/main.xml
@@ -0,0 +1,48 @@
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/values-w820dp/dimens.xml b/AndroidYahooWeather/weather/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/AndroidYahooWeather/weather/src/main/res/values/colors.xml b/AndroidYahooWeather/weather/src/main/res/values/colors.xml
new file mode 100644
index 0000000..94632dd
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/values/colors.xml
@@ -0,0 +1,22 @@
+
+
+
+ #000000
+
+
\ No newline at end of file
diff --git a/AndroidYahooWeather/weather/src/main/res/values/dimens.xml b/AndroidYahooWeather/weather/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..a0171a7
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/values/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 16dp
+ 16dp
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/values/strings.xml b/AndroidYahooWeather/weather/src/main/res/values/strings.xml
new file mode 100644
index 0000000..581384c
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/values/strings.xml
@@ -0,0 +1,40 @@
+
+
+
+ WeatherApp
+ Hello world!
+ Settings
+ Refresh
+ Share
+ Donate
+ data provided by Yahoo! Weather
+ Last update
+ There is a new weather app in the play store. Look here
+
+
+ City
+
+
+ Measure unit
+ Temperature unit
+ Location
+ Choose the location
+
+ Current location
+ Current temperature unit
+
+
+ If you like my work and want to use the source code, please donate a beer to me
+ Donate!
+ Cancel
+
+
+ °C
+ °F
+
+
+ c
+ f
+
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/values/styles.xml b/AndroidYahooWeather/weather/src/main/res/values/styles.xml
new file mode 100644
index 0000000..26c0c2e
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/values/styles.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AndroidYahooWeather/weather/src/main/res/xml/weather_prefs.xml b/AndroidYahooWeather/weather/src/main/res/xml/weather_prefs.xml
new file mode 100644
index 0000000..8b1c92c
--- /dev/null
+++ b/AndroidYahooWeather/weather/src/main/res/xml/weather_prefs.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file