Initial commit
authorMarius Gavrilescu <marius@ieval.ro>
Tue, 5 Mar 2013 19:12:17 +0000 (21:12 +0200)
committerMarius Gavrilescu <marius@ieval.ro>
Tue, 5 Mar 2013 19:12:17 +0000 (21:12 +0200)
62 files changed:
.classpath [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.pmd [new file with mode: 0644]
.project [new file with mode: 0644]
.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
AndroidManifest.xml [new file with mode: 0644]
COPYING [new file with mode: 0644]
lib/android/support/v4/app/NotificationCompat.java [new file with mode: 0644]
lib/android/support/v4/app/NotificationCompatHoneycomb.java [new file with mode: 0644]
lib/android/support/v4/app/NotificationCompatIceCreamSandwich.java [new file with mode: 0644]
lib/android/support/v4/app/NotificationCompatJellybean.java [new file with mode: 0644]
lib/android/support/v4/content/LocalBroadcastManager.java [new file with mode: 0644]
lib/com/google/android/gcm/GCMBaseIntentService.java [new file with mode: 0644]
lib/com/google/android/gcm/GCMBroadcastReceiver.java [new file with mode: 0644]
lib/com/google/android/gcm/GCMConstants.java [new file with mode: 0644]
lib/com/google/android/gcm/GCMRegistrar.java [new file with mode: 0644]
proguard-project.txt [new file with mode: 0644]
project.properties [new file with mode: 0644]
res/drawable-hdpi/cross.png [new file with mode: 0644]
res/drawable-hdpi/tick.png [new file with mode: 0644]
res/drawable-ldpi/cross.png [new file with mode: 0644]
res/drawable-ldpi/tick.png [new file with mode: 0644]
res/drawable-mdpi/cross.png [new file with mode: 0644]
res/drawable-mdpi/tick.png [new file with mode: 0644]
res/drawable-xhdpi/cross.png [new file with mode: 0644]
res/drawable-xhdpi/tick.png [new file with mode: 0644]
res/layout/local.xml [new file with mode: 0644]
res/layout/main.xml [new file with mode: 0644]
res/layout/ongoing_list_item.xml [new file with mode: 0644]
res/menu/main.xml [new file with mode: 0644]
res/values/strings.xml [new file with mode: 0644]
res/xml/admin.xml [new file with mode: 0644]
res/xml/prefs.xml [new file with mode: 0644]
src/com/android/internal/telephony/ITelephony.aidl [new file with mode: 0644]
src/org/eclipse/jdt/annotation/NonNull.java [new file with mode: 0644]
src/org/eclipse/jdt/annotation/NonNullByDefault.java [new file with mode: 0644]
src/org/eclipse/jdt/annotation/Nullable.java [new file with mode: 0644]
src/org/eclipse/jdt/annotation/package-info.java [new file with mode: 0644]
src/ro/ieval/fonbot/Address.java [new file with mode: 0644]
src/ro/ieval/fonbot/BackupAgent.java [new file with mode: 0644]
src/ro/ieval/fonbot/DialogActivity.java [new file with mode: 0644]
src/ro/ieval/fonbot/DynamicEventReceiver.java [new file with mode: 0644]
src/ro/ieval/fonbot/FonBotAdminReceiver.java [new file with mode: 0644]
src/ro/ieval/fonbot/FonBotApplication.java [new file with mode: 0644]
src/ro/ieval/fonbot/FonBotHelpActivity.java [new file with mode: 0644]
src/ro/ieval/fonbot/FonBotLocalActivity.java [new file with mode: 0644]
src/ro/ieval/fonbot/FonBotMainActivity.java [new file with mode: 0644]
src/ro/ieval/fonbot/FonBotMainService.java [new file with mode: 0644]
src/ro/ieval/fonbot/FonBotPhoneStateListener.java [new file with mode: 0644]
src/ro/ieval/fonbot/FonBotPreferenceActivity.java [new file with mode: 0644]
src/ro/ieval/fonbot/GCMIntentService.java [new file with mode: 0644]
src/ro/ieval/fonbot/Header.java [new file with mode: 0644]
src/ro/ieval/fonbot/Heavy.java [new file with mode: 0644]
src/ro/ieval/fonbot/LocalBroadcastReceiver.java [new file with mode: 0644]
src/ro/ieval/fonbot/PollServerAsyncTask.java [new file with mode: 0644]
src/ro/ieval/fonbot/ProtectedBroadcastReceiver.java [new file with mode: 0644]
src/ro/ieval/fonbot/RemoteCrashdumpHandler.java [new file with mode: 0644]
src/ro/ieval/fonbot/SendHttpMessageAsyncTask.java [new file with mode: 0644]
src/ro/ieval/fonbot/SmsReceiver.java [new file with mode: 0644]
src/ro/ieval/fonbot/SmsStatusReceiver.java [new file with mode: 0644]
src/ro/ieval/fonbot/Utils.java [new file with mode: 0644]
src/ro/ieval/fonbot/package-info.java [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..2735db2
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="gen">
+               <attributes>
+                       <attribute name="ignore_optional_problems" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry kind="src" path="lib">
+               <attributes>
+                       <attribute name="ignore_optional_problems" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+       <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+       <classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..7792e06
--- /dev/null
@@ -0,0 +1,2 @@
+bin/
+gen/
diff --git a/.pmd b/.pmd
new file mode 100644 (file)
index 0000000..5d5f745
--- /dev/null
+++ b/.pmd
@@ -0,0 +1,849 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<pmd>
+    <useProjectRuleSet>false</useProjectRuleSet>
+    <ruleSetFile>.ruleset</ruleSetFile>
+    <rules>
+        <rule>
+            <name>LooseCoupling</name>
+            <ruleset>Type Resolution Rules</ruleset>
+        </rule>
+        <rule>
+            <name>CloneMethodMustImplementCloneable</name>
+            <ruleset>Type Resolution Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SignatureDeclareThrowsException</name>
+            <ruleset>Type Resolution Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseSingleton</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SimplifyBooleanReturns</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SimplifyBooleanExpressions</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidDeeplyNestedIfStmts</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidReassigningParameters</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SwitchDensity</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ConstructorCallsOverridableMethod</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AccessorClassGeneration</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>FinalFieldCouldBeStatic</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>CloseResource</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>NonStaticInitializer</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DefaultLabelNotLastInSwitchStmt</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>NonCaseLabelInSwitchStatement</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>OptimizableToArrayCall</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>BadComparison</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EqualsNull</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ConfusingTernary</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>InstantiationToGetClass</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>IdempotentOperations</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SimpleDateFormatNeedsLocale</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ImmutableField</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseLocaleWithCaseConversions</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidProtectedFieldInFinalClass</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AssignmentToNonFinalStatic</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MissingStaticMethodInNonInstantiatableClass</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidSynchronizedAtMethodLevel</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MissingBreakInSwitch</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseNotifyAllInsteadOfNotify</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidInstanceofChecksInCatchClause</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AbstractClassWithoutAbstractMethod</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SimplifyConditional</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>CompareObjectsWithEquals</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>PositionLiteralsFirstInComparisons</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryLocalBeforeReturn</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>NonThreadSafeSingleton</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UncommentedEmptyMethod</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UncommentedEmptyConstructor</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidConstantsInterface</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnsynchronizedStaticDateFormatter</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>PreserveStackTrace</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseCollectionIsEmpty</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ClassWithOnlyPrivateConstructorsShouldBeFinal</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyMethodInAbstractClassShouldBeAbstract</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SingularField</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ReturnEmptyArrayRatherThanNull</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AbstractClassWithoutAnyMethod</name>
+            <ruleset>Design Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidCatchingThrowable</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SignatureDeclareThrowsException</name>
+            <ruleset>Type Resolution Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ExceptionAsFlowControl</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidCatchingNPE</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidThrowingRawExceptionTypes</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidThrowingNullPointerException</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidRethrowingException</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DoNotExtendJavaLangError</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DoNotThrowExceptionInFinally</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidThrowingNewInstanceOfSameException</name>
+            <ruleset>Strict Exception Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnusedPrivateField</name>
+            <ruleset>Unused Code Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnusedLocalVariable</name>
+            <ruleset>Unused Code Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnusedPrivateMethod</name>
+            <ruleset>Unused Code Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnusedFormalParameter</name>
+            <ruleset>Unused Code Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MoreThanOneLogger</name>
+            <ruleset>Java Logging Rules</ruleset>
+        </rule>
+        <rule>
+            <name>LoggerIsNotStaticFinal</name>
+            <ruleset>Java Logging Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SystemPrintln</name>
+            <ruleset>Java Logging Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidPrintStackTrace</name>
+            <ruleset>Java Logging Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidDuplicateLiterals</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>StringInstantiation</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>StringToString</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>InefficientStringBuffering</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryCaseChange</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseStringBufferLength</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AppendCharacterWithChar</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ConsecutiveLiteralAppends</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseIndexOfChar</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>InefficientEmptyStringCheck</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>InsufficientStringBufferDeclaration</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UselessStringValueOf</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>StringBufferInstantiationWithChar</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseEqualsToCompareStrings</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidStringBufferField</name>
+            <ruleset>String and StringBuffer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ReplaceVectorWithList</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ReplaceHashtableWithMap</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ReplaceEnumerationWithIterator</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidEnumAsIdentifier</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidAssertAsIdentifier</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>IntegerInstantiation</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ByteInstantiation</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ShortInstantiation</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>LongInstantiation</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnit4TestShouldUseBeforeAnnotation</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnit4TestShouldUseAfterAnnotation</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnit4TestShouldUseTestAnnotation</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnit4SuitesShouldUseSuiteAnnotation</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnitUseExpected</name>
+            <ruleset>Migration Rules</ruleset>
+        </rule>
+        <rule>
+            <name>LocalVariableCouldBeFinal</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MethodArgumentCouldBeFinal</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidInstantiatingObjectsInLoops</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseArrayListInsteadOfVector</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SimplifyStartsWith</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseStringBufferForStringAppends</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseArraysAsList</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidArrayLoops</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryWrapperObjectCreation</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AddEmptyString</name>
+            <ruleset>Optimization Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyCatchBlock</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyIfStmt</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyWhileStmt</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyTryBlock</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyFinallyBlock</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptySwitchStatements</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JumbledIncrementer</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ForLoopShouldBeWhileLoop</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryConversionTemporary</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>OverrideBothEqualsAndHashcode</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DoubleCheckedLocking</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ReturnFromFinallyBlock</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptySynchronizedBlock</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryReturn</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyStaticInitializer</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnconditionalIfStatement</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyStatementNotInLoop</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>BooleanInstantiation</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryFinalModifier</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>CollapsibleIfStatements</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UselessOverridingMethod</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ClassCastExceptionWithToArray</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidDecimalLiteralsInBigDecimalConstructor</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UselessOperationOnImmutable</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MisplacedNullCheck</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnusedNullCheckInEquals</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidThreadGroup</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>BrokenNullCheck</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>BigIntegerInstantiation</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidUsingOctalValues</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidUsingHardCodedIP</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>CheckResultSet</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidMultipleUnaryOperators</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyInitializer</name>
+            <ruleset>Basic Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MethodReturnsInternalArray</name>
+            <ruleset>Security Code Guidelines</ruleset>
+        </rule>
+        <rule>
+            <name>ArrayIsStoredDirectly</name>
+            <ruleset>Security Code Guidelines</ruleset>
+        </rule>
+        <rule>
+            <name>CouplingBetweenObjects</name>
+            <ruleset>Coupling Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ExcessiveImports</name>
+            <ruleset>Coupling Rules</ruleset>
+        </rule>
+        <rule>
+            <name>LooseCoupling</name>
+            <ruleset>Type Resolution Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DuplicateImports</name>
+            <ruleset>Import Statement Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DontImportJavaLang</name>
+            <ruleset>Import Statement Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ImportFromSamePackage</name>
+            <ruleset>Import Statement Rules</ruleset>
+        </rule>
+        <rule>
+            <name>TooManyStaticImports</name>
+            <ruleset>Import Statement Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnitStaticSuite</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnitSpelling</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnitAssertionsShouldIncludeMessage</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>JUnitTestsShouldIncludeAssert</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>TestClassWithoutTestCases</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryBooleanAssertion</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseAssertEqualsInsteadOfAssertTrue</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseAssertSameInsteadOfAssertTrue</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseAssertNullInsteadOfAssertTrue</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SimplifyBooleanAssertion</name>
+            <ruleset>JUnit Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryConstructor</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>NullAssignment</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnusedModifier</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AssignmentInOperand</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AtLeastOneConstructor</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DontImportSun</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SuspiciousOctalEscape</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>CallSuperInConstructor</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UnnecessaryParentheses</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DefaultPackage</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>BooleanInversion</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidUsingShortType</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidUsingVolatile</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidUsingNativeCode</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidAccessibilityAlteration</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>DoNotCallGarbageCollectionExplicitly</name>
+            <ruleset>Controversial Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ShortMethodName</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>VariableNamingConventions</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MethodNamingConventions</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ClassNamingConventions</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AbstractNaming</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidDollarSigns</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MethodWithSameNameAsEnclosingClass</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SuspiciousHashcodeMethodName</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SuspiciousConstantFieldName</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>SuspiciousEqualsMethodName</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidFieldNameMatchingTypeName</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidFieldNameMatchingMethodName</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>NoPackage</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>PackageCase</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MisleadingVariableName</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>BooleanGetMethodName</name>
+            <ruleset>Naming Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ExcessiveMethodLength</name>
+            <ruleset>Code Size Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ExcessiveParameterList</name>
+            <ruleset>Code Size Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ExcessiveClassLength</name>
+            <ruleset>Code Size Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ExcessivePublicCount</name>
+            <ruleset>Code Size Rules</ruleset>
+        </rule>
+        <rule>
+            <name>NcssMethodCount</name>
+            <ruleset>Code Size Rules</ruleset>
+        </rule>
+        <rule>
+            <name>NcssTypeCount</name>
+            <ruleset>Code Size Rules</ruleset>
+        </rule>
+        <rule>
+            <name>NcssConstructorCount</name>
+            <ruleset>Code Size Rules</ruleset>
+        </rule>
+        <rule>
+            <name>EmptyFinalizer</name>
+            <ruleset>Finalizer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>FinalizeOnlyCallsSuperFinalize</name>
+            <ruleset>Finalizer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>FinalizeOverloaded</name>
+            <ruleset>Finalizer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>FinalizeDoesNotCallSuperFinalize</name>
+            <ruleset>Finalizer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>FinalizeShouldBeProtected</name>
+            <ruleset>Finalizer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>AvoidCallingFinalize</name>
+            <ruleset>Finalizer Rules</ruleset>
+        </rule>
+        <rule>
+            <name>UseCorrectExceptionLogging</name>
+            <ruleset>Jakarta Commons Logging Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ProperLogger</name>
+            <ruleset>Jakarta Commons Logging Rules</ruleset>
+        </rule>
+        <rule>
+            <name>MissingSerialVersionUID</name>
+            <ruleset>JavaBean Rules</ruleset>
+        </rule>
+        <rule>
+            <name>ProperCloneImplementation</name>
+            <ruleset>Clone Implementation Rules</ruleset>
+        </rule>
+        <rule>
+            <name>CloneThrowsCloneNotSupportedException</name>
+            <ruleset>Clone Implementation Rules</ruleset>
+        </rule>
+        <rule>
+            <name>CloneMethodMustImplementCloneable</name>
+            <ruleset>Type Resolution Rules</ruleset>
+        </rule>
+    </rules>
+    <includeDerivedFiles>false</includeDerivedFiles>
+    <violationsAsErrors>true</violationsAsErrors>
+</pmd>
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..799c0bc
--- /dev/null
+++ b/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>FonBot</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..8000cd6
--- /dev/null
@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..f9e15c2
--- /dev/null
@@ -0,0 +1,200 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="ro.ieval.fonbot"
+    android:installLocation="internalOnly"
+    android:versionCode="1"
+    android:versionName="prerelease" >
+
+    <uses-sdk
+        android:minSdkVersion="8"
+        android:targetSdkVersion="17" />
+
+    <permission
+        android:name="ro.ieval.fonbot.permission.C2D_MESSAGE"
+        android:label="Receive GCM messages"
+        android:protectionLevel="signature" >
+    </permission>
+
+    <uses-permission android:name="ro.ieval.fonbot.permission.C2D_MESSAGE" />
+    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+    <uses-permission android:name="android.permission.CALL_PRIVILEGED" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.FLASHLIGHT" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.READ_SMS" />
+    <uses-permission android:name="android.permission.REBOOT" />
+    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+
+    <permission
+        android:name="ro.ieval.fonbot.permission.SEND_NOTIFICATIONS"
+        android:description="@string/send_message_permission_desc"
+        android:label="send FonBot notifications" >
+    </permission>
+
+    <uses-feature
+        android:name="android.hardware.bluetooth"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.camera"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.camera.autofocus"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.camera.flash"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.camera.front"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.camera.any"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.location"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.location.network"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.location.gps"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.telephony"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.touchscreen"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.wifi"
+        android:required="false" />
+
+    <application
+        android:name=".FonBotApplication"
+        android:allowBackup="true"
+        android:backupAgent="BackupAgent"
+        android:enabled="true"
+        android:hasCode="true"
+        android:label="@string/app_name"
+        android:persistent="false" >
+        <activity
+            android:name=".FonBotHelpActivity"
+            android:alwaysRetainTaskState="false"
+            android:excludeFromRecents="true"
+            android:exported="false"
+            android:immersive="false"
+            android:label="@string/help_activity_label"
+            android:launchMode="standard"
+            android:multiprocess="false"
+            android:parentActivityName=".FonBotMainActivity"
+            android:stateNotNeeded="true"
+            android:windowSoftInputMode="stateAlwaysHidden" >
+        </activity>
+        <activity
+            android:name=".FonBotMainActivity"
+            android:immersive="false"
+            android:launchMode="standard"
+            android:multiprocess="false"
+            android:screenOrientation="unspecified"
+            android:stateNotNeeded="true"
+            android:uiOptions="splitActionBarWhenNarrow"
+            android:windowSoftInputMode="stateAlwaysHidden" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".FonBotLocalActivity"
+            android:exported="false" >
+        </activity>
+        <activity
+            android:name=".FonBotPreferenceActivity"
+            android:exported="false" >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="ro.ieval.fonbot.FonBotPreferenceActivity" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".DialogActivity"
+            android:exported="false"
+            android:noHistory="true"
+            android:theme="@android:style/Theme.Dialog" >
+        </activity>
+
+        <service android:name=".GCMIntentService" >
+        </service>
+        <service android:name=".FonBotMainService" >
+        </service>
+
+        <receiver
+            android:name="com.google.android.gcm.GCMBroadcastReceiver"
+            android:permission="com.google.android.c2dm.permission.SEND" >
+            <intent-filter>
+                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
+
+                <category android:name="ro.ieval.fonbot" />
+            </intent-filter>
+        </receiver>
+        <receiver
+            android:name=".SmsReceiver"
+            android:permission="android.permission.BROADCAST_SMS" >
+            <intent-filter android:priority="2147483647" >
+                <action android:name="android.provider.Telephony.SMS_RECEIVED" />
+            </intent-filter>
+        </receiver>
+        <receiver android:name=".SmsStatusReceiver" >
+        </receiver>
+        <receiver
+            android:name=".FonBotAdminReceiver"
+            android:exported="false"
+            android:permission="android.permission.BIND_DEVICE_ADMIN" >
+            <intent-filter>
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.app.device_admin"
+                android:resource="@xml/admin" />
+        </receiver>
+        <receiver
+            android:name=".ProtectedBroadcastReceiver"
+            android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.BATTERY_LOW" />
+                <action android:name="android.intent.action.BATTERY_OKAY" />
+            </intent-filter>
+        </receiver>
+        <receiver
+            android:name=".LocalBroadcastReceiver"
+            android:exported="false" >
+            <intent-filter>
+                <action android:name="ro.ieval.fonbot.LocalBroadcastReceiver.ACTION_POLL_ALARM" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/lib/android/support/v4/app/NotificationCompat.java b/lib/android/support/v4/app/NotificationCompat.java
new file mode 100644 (file)
index 0000000..03881b0
--- /dev/null
@@ -0,0 +1,823 @@
+/*
+ * 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 android.support.v4.app;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.widget.RemoteViews;
+import java.util.ArrayList;
+
+/**
+ * Helper for accessing features in {@link android.app.Notification}
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public class NotificationCompat {
+    /**
+     * Obsolete flag indicating high-priority notifications; use the priority field instead.
+     *
+     * @deprecated Use {@link NotificationCompat.Builder#setPriority(int)} with a positive value.
+     */
+    @Deprecated
+       public static final int FLAG_HIGH_PRIORITY = 0x00000080;
+
+    /**
+     * Default notification priority for {@link NotificationCompat.Builder#setPriority(int)}.
+     * If your application does not prioritize its own notifications,
+     * use this value for all notifications.
+     */
+    public static final int PRIORITY_DEFAULT = 0;
+
+    /**
+     * Lower notification priority for {@link NotificationCompat.Builder#setPriority(int)},
+     * for items that are less important. The UI may choose to show
+     * these items smaller, or at a different position in the list,
+     * compared with your app's {@link #PRIORITY_DEFAULT} items.
+     */
+    public static final int PRIORITY_LOW = -1;
+
+    /**
+     * Lowest notification priority for {@link NotificationCompat.Builder#setPriority(int)};
+     * these items might not be shown to the user except under
+     * special circumstances, such as detailed notification logs.
+     */
+    public static final int PRIORITY_MIN = -2;
+
+    /**
+     * Higher notification priority for {@link NotificationCompat.Builder#setPriority(int)},
+     * for more important notifications or alerts. The UI may choose
+     * to show these items larger, or at a different position in
+     * notification lists, compared with your app's {@link #PRIORITY_DEFAULT} items.
+     */
+    public static final int PRIORITY_HIGH = 1;
+
+    /**
+     * Highest notification priority for {@link NotificationCompat.Builder#setPriority(int)},
+     * for your application's most important items that require the user's
+     * prompt attention or input.
+     */
+    public static final int PRIORITY_MAX = 2;
+
+    private static final NotificationCompatImpl IMPL;
+
+    interface NotificationCompatImpl {
+        public Notification build(Builder b);
+    }
+
+    static class NotificationCompatImplBase implements NotificationCompatImpl {
+        @SuppressWarnings("deprecation")
+               @Override
+               public Notification build(Builder b) {
+            Notification result = b.mNotification;
+            result.setLatestEventInfo(b.mContext, b.mContentTitle,
+                    b.mContentText, b.mContentIntent);
+            // translate high priority requests into legacy flag
+            if (b.mPriority > PRIORITY_DEFAULT) {
+                result.flags |= FLAG_HIGH_PRIORITY;
+            }
+            return result;
+        }
+    }
+
+    static class NotificationCompatImplHoneycomb implements NotificationCompatImpl {
+        @Override
+               public Notification build(Builder b) {
+            return NotificationCompatHoneycomb.add(b.mContext, b.mNotification,
+                    b.mContentTitle, b.mContentText, b.mContentInfo, b.mTickerView,
+                    b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon);
+        }
+    }
+
+    static class NotificationCompatImplIceCreamSandwich implements NotificationCompatImpl {
+        @Override
+               public Notification build(Builder b) {
+            return NotificationCompatIceCreamSandwich.add(b.mContext, b.mNotification,
+                    b.mContentTitle, b.mContentText, b.mContentInfo, b.mTickerView,
+                    b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
+                    b.mProgressMax, b.mProgress, b.mProgressIndeterminate);
+        }
+    }
+
+    static class NotificationCompatImplJellybean implements NotificationCompatImpl {
+        @Override
+               public Notification build(Builder b) {
+            NotificationCompatJellybean jbBuilder = new NotificationCompatJellybean(
+                    b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
+                    b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
+                    b.mProgressMax, b.mProgress, b.mProgressIndeterminate,
+                    b.mUseChronometer, b.mPriority, b.mSubText);
+            for (Action action: b.mActions) {
+                jbBuilder.addAction(action.icon, action.title, action.actionIntent);
+            }
+            if (b.mStyle != null) {
+                if (b.mStyle instanceof BigTextStyle) {
+                    BigTextStyle style = (BigTextStyle) b.mStyle;
+                    jbBuilder.addBigTextStyle(style.mBigContentTitle,
+                            style.mSummaryTextSet,
+                            style.mSummaryText,
+                            style.mBigText);
+                } else if (b.mStyle instanceof InboxStyle) {
+                    InboxStyle style = (InboxStyle) b.mStyle;
+                    jbBuilder.addInboxStyle(style.mBigContentTitle,
+                            style.mSummaryTextSet,
+                            style.mSummaryText,
+                            style.mTexts);
+                } else if (b.mStyle instanceof BigPictureStyle) {
+                    BigPictureStyle style = (BigPictureStyle) b.mStyle;
+                    jbBuilder.addBigPictureStyle(style.mBigContentTitle,
+                            style.mSummaryTextSet,
+                            style.mSummaryText,
+                            style.mPicture);
+                }
+            }
+            return(jbBuilder.build());
+        }
+    }
+
+    static {
+        if (Build.VERSION.SDK_INT >= 16) {
+            IMPL = new NotificationCompatImplJellybean();
+        } else if (Build.VERSION.SDK_INT >= 14) {
+            IMPL = new NotificationCompatImplIceCreamSandwich();
+        } else if (Build.VERSION.SDK_INT >= 11) {
+            IMPL = new NotificationCompatImplHoneycomb();
+        } else {
+            IMPL = new NotificationCompatImplBase();
+        }
+    }
+
+    /**
+     * Builder class for {@link NotificationCompat} objects.  Allows easier control over
+     * all the flags, as well as help constructing the typical notification layouts.
+     * <p>
+     * On platform versions that don't offer expanded notifications, methods that depend on
+     * expanded notifications have no effect.
+     * </p>
+     * <p>
+     * For example, action buttons won't appear on platforms prior to Android 4.1. Action
+     * buttons depend on expanded notifications, which are only available in Android 4.1
+     * and later.
+     * <p>
+     * For this reason, you should always ensure that UI controls in a notification are also
+     * available in an {@link android.app.Activity} in your app, and you should always start that
+     * {@link android.app.Activity} when users click the notification. To do this, use the
+     * {@link NotificationCompat.Builder#setContentIntent setContentIntent()}
+     * method.
+     * </p>
+     *
+     */
+    public static class Builder {
+        Context mContext;
+
+        CharSequence mContentTitle;
+        CharSequence mContentText;
+        PendingIntent mContentIntent;
+        PendingIntent mFullScreenIntent;
+        RemoteViews mTickerView;
+        Bitmap mLargeIcon;
+        CharSequence mContentInfo;
+        int mNumber;
+        int mPriority;
+        boolean mUseChronometer;
+        Style mStyle;
+        CharSequence mSubText;
+        int mProgressMax;
+        int mProgress;
+        boolean mProgressIndeterminate;
+        ArrayList<Action> mActions = new ArrayList<Action>();
+
+        Notification mNotification = new Notification();
+
+        /**
+         * Constructor.
+         *
+         * Automatically sets the when field to {@link System#currentTimeMillis()
+         * System.currentTimeMillis()} and the audio stream to the
+         * {@link Notification#STREAM_DEFAULT}.
+         *
+         * @param context A {@link Context} that will be used to construct the
+         *      RemoteViews. The Context will not be held past the lifetime of this
+         *      Builder object.
+         */
+        public Builder(Context context) {
+            mContext = context;
+
+            // Set defaults to match the defaults of a Notification
+            mNotification.when = System.currentTimeMillis();
+            mNotification.audioStreamType = Notification.STREAM_DEFAULT;
+            mPriority = PRIORITY_DEFAULT;
+        }
+
+        /**
+         * Set the time that the event occurred.  Notifications in the panel are
+         * sorted by this time.
+         */
+        public Builder setWhen(long when) {
+            mNotification.when = when;
+            return this;
+        }
+
+        /**
+         * Show the {@link Notification#when} field as a stopwatch.
+         *
+         * Instead of presenting <code>when</code> as a timestamp, the notification will show an
+         * automatically updating display of the minutes and seconds since <code>when</code>.
+         *
+         * Useful when showing an elapsed time (like an ongoing phone call).
+         *
+         * @see android.widget.Chronometer
+         * @see Notification#when
+         */
+        public Builder setUsesChronometer(boolean b) {
+            mUseChronometer = b;
+            return this;
+        }
+
+        /**
+         * Set the small icon to use in the notification layouts.  Different classes of devices
+         * may return different sizes.  See the UX guidelines for more information on how to
+         * design these icons.
+         *
+         * @param icon A resource ID in the application's package of the drawble to use.
+         */
+        public Builder setSmallIcon(int icon) {
+            mNotification.icon = icon;
+            return this;
+        }
+
+        /**
+         * A variant of {@link #setSmallIcon(int) setSmallIcon(int)} that takes an additional
+         * level parameter for when the icon is a {@link android.graphics.drawable.LevelListDrawable
+         * LevelListDrawable}.
+         *
+         * @param icon A resource ID in the application's package of the drawble to use.
+         * @param level The level to use for the icon.
+         *
+         * @see android.graphics.drawable.LevelListDrawable
+         */
+        public Builder setSmallIcon(int icon, int level) {
+            mNotification.icon = icon;
+            mNotification.iconLevel = level;
+            return this;
+        }
+
+        /**
+         * Set the title (first row) of the notification, in a standard notification.
+         */
+        public Builder setContentTitle(CharSequence title) {
+            mContentTitle = title;
+            return this;
+        }
+
+        /**
+         * Set the text (second row) of the notification, in a standard notification.
+         */
+        public Builder setContentText(CharSequence text) {
+            mContentText = text;
+            return this;
+        }
+
+        /**
+         * Set the third line of text in the platform notification template.
+         * Don't use if you're also using {@link #setProgress(int, int, boolean)};
+         * they occupy the same location in the standard template.
+         * <br>
+         * If the platform does not provide large-format notifications, this method has no effect.
+         * The third line of text only appears in expanded view.
+         * <br>
+         */
+        public Builder setSubText(CharSequence text) {
+            mSubText = text;
+            return this;
+        }
+
+        /**
+         * Set the large number at the right-hand side of the notification.  This is
+         * equivalent to setContentInfo, although it might show the number in a different
+         * font size for readability.
+         */
+        public Builder setNumber(int number) {
+            mNumber = number;
+            return this;
+        }
+
+        /**
+         * Set the large text at the right-hand side of the notification.
+         */
+        public Builder setContentInfo(CharSequence info) {
+            mContentInfo = info;
+            return this;
+        }
+
+        /**
+         * Set the progress this notification represents, which may be
+         * represented as a {@link android.widget.ProgressBar}.
+         */
+        public Builder setProgress(int max, int progress, boolean indeterminate) {
+            mProgressMax = max;
+            mProgress = progress;
+            mProgressIndeterminate = indeterminate;
+            return this;
+        }
+
+        /**
+         * Supply a custom RemoteViews to use instead of the standard one.
+         */
+        public Builder setContent(RemoteViews views) {
+            mNotification.contentView = views;
+            return this;
+        }
+
+        /**
+         * Supply a {@link PendingIntent} to send when the notification is clicked.
+         * If you do not supply an intent, you can now add PendingIntents to individual
+         * views to be launched when clicked by calling {@link RemoteViews#setOnClickPendingIntent
+         * RemoteViews.setOnClickPendingIntent(int,PendingIntent)}.  Be sure to
+         * read {@link Notification#contentIntent Notification.contentIntent} for
+         * how to correctly use this.
+         */
+        public Builder setContentIntent(PendingIntent intent) {
+            mContentIntent = intent;
+            return this;
+        }
+
+        /**
+         * Supply a {@link PendingIntent} to send when the notification is cleared by the user
+         * directly from the notification panel.  For example, this intent is sent when the user
+         * clicks the "Clear all" button, or the individual "X" buttons on notifications.  This
+         * intent is not sent when the application calls {@link NotificationManager#cancel
+         * NotificationManager.cancel(int)}.
+         */
+        public Builder setDeleteIntent(PendingIntent intent) {
+            mNotification.deleteIntent = intent;
+            return this;
+        }
+
+        /**
+         * An intent to launch instead of posting the notification to the status bar.
+         * Only for use with extremely high-priority notifications demanding the user's
+         * <strong>immediate</strong> attention, such as an incoming phone call or
+         * alarm clock that the user has explicitly set to a particular time.
+         * If this facility is used for something else, please give the user an option
+         * to turn it off and use a normal notification, as this can be extremely
+         * disruptive.
+         *
+         * @param intent The pending intent to launch.
+         * @param highPriority Passing true will cause this notification to be sent
+         *          even if other notifications are suppressed.
+         */
+        public Builder setFullScreenIntent(PendingIntent intent, boolean highPriority) {
+            mFullScreenIntent = intent;
+            setFlag(FLAG_HIGH_PRIORITY, highPriority);
+            return this;
+        }
+
+        /**
+         * Set the text that is displayed in the status bar when the notification first
+         * arrives.
+         */
+        public Builder setTicker(CharSequence tickerText) {
+            mNotification.tickerText = tickerText;
+            return this;
+        }
+
+        /**
+         * Set the text that is displayed in the status bar when the notification first
+         * arrives, and also a RemoteViews object that may be displayed instead on some
+         * devices.
+         */
+        public Builder setTicker(CharSequence tickerText, RemoteViews views) {
+            mNotification.tickerText = tickerText;
+            mTickerView = views;
+            return this;
+        }
+
+        /**
+         * Set the large icon that is shown in the ticker and notification.
+         */
+        public Builder setLargeIcon(Bitmap icon) {
+            mLargeIcon = icon;
+            return this;
+        }
+
+        /**
+         * Set the sound to play.  It will play on the default stream.
+         */
+        public Builder setSound(Uri sound) {
+            mNotification.sound = sound;
+            mNotification.audioStreamType = Notification.STREAM_DEFAULT;
+            return this;
+        }
+
+        /**
+         * Set the sound to play.  It will play on the stream you supply.
+         *
+         * @see #STREAM_DEFAULT
+         * @see AudioManager for the <code>STREAM_</code> constants.
+         */
+        public Builder setSound(Uri sound, int streamType) {
+            mNotification.sound = sound;
+            mNotification.audioStreamType = streamType;
+            return this;
+        }
+
+        /**
+         * Set the vibration pattern to use.
+         *
+         * @see android.os.Vibrator for a discussion of the <code>pattern</code>
+         * parameter.
+         */
+        public Builder setVibrate(long[] pattern) {
+            mNotification.vibrate = pattern;
+            return this;
+        }
+
+        /**
+         * Set the argb value that you would like the LED on the device to blnk, as well as the
+         * rate.  The rate is specified in terms of the number of milliseconds to be on
+         * and then the number of milliseconds to be off.
+         */
+        public Builder setLights(int argb, int onMs, int offMs) {
+            mNotification.ledARGB = argb;
+            mNotification.ledOnMS = onMs;
+            mNotification.ledOffMS = offMs;
+            boolean showLights = mNotification.ledOnMS != 0 && mNotification.ledOffMS != 0;
+            mNotification.flags = (mNotification.flags & ~Notification.FLAG_SHOW_LIGHTS) |
+                    (showLights ? Notification.FLAG_SHOW_LIGHTS : 0);
+            return this;
+        }
+
+        /**
+         * Set whether this is an ongoing notification.
+         *
+         * <p>Ongoing notifications differ from regular notifications in the following ways:
+         * <ul>
+         *   <li>Ongoing notifications are sorted above the regular notifications in the
+         *   notification panel.</li>
+         *   <li>Ongoing notifications do not have an 'X' close button, and are not affected
+         *   by the "Clear all" button.
+         * </ul>
+         */
+        public Builder setOngoing(boolean ongoing) {
+            setFlag(Notification.FLAG_ONGOING_EVENT, ongoing);
+            return this;
+        }
+
+        /**
+         * Set this flag if you would only like the sound, vibrate
+         * and ticker to be played if the notification is not already showing.
+         */
+        public Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
+            setFlag(Notification.FLAG_ONLY_ALERT_ONCE, onlyAlertOnce);
+            return this;
+        }
+
+        /**
+         * Setting this flag will make it so the notification is automatically
+         * canceled when the user clicks it in the panel.  The PendingIntent
+         * set with {@link #setDeleteIntent} will be broadcast when the notification
+         * is canceled.
+         */
+        public Builder setAutoCancel(boolean autoCancel) {
+            setFlag(Notification.FLAG_AUTO_CANCEL, autoCancel);
+            return this;
+        }
+
+        /**
+         * Set the default notification options that will be used.
+         * <p>
+         * The value should be one or more of the following fields combined with
+         * bitwise-or:
+         * {@link Notification#DEFAULT_SOUND}, {@link Notification#DEFAULT_VIBRATE},
+         * {@link Notification#DEFAULT_LIGHTS}.
+         * <p>
+         * For all default values, use {@link Notification#DEFAULT_ALL}.
+         */
+        public Builder setDefaults(int defaults) {
+            mNotification.defaults = defaults;
+            if ((defaults & Notification.DEFAULT_LIGHTS) != 0) {
+                mNotification.flags |= Notification.FLAG_SHOW_LIGHTS;
+            }
+            return this;
+        }
+
+        private void setFlag(int mask, boolean value) {
+            if (value) {
+                mNotification.flags |= mask;
+            } else {
+                mNotification.flags &= ~mask;
+            }
+        }
+
+        /**
+         * Set the relative priority for this notification.
+         *
+         * Priority is an indication of how much of the user's
+         * valuable attention should be consumed by this
+         * notification. Low-priority notifications may be hidden from
+         * the user in certain situations, while the user might be
+         * interrupted for a higher-priority notification.
+         * The system sets a notification's priority based on various factors including the
+         * setPriority value. The effect may differ slightly on different platforms.
+         */
+        public Builder setPriority(int pri) {
+            mPriority = pri;
+            return this;
+        }
+
+        /**
+         * Add an action to this notification. Actions are typically displayed by
+         * the system as a button adjacent to the notification content.
+         * <br>
+         * Action buttons won't appear on platforms prior to Android 4.1. Action
+         * buttons depend on expanded notifications, which are only available in Android 4.1
+         * and later. To ensure that an action button's functionality is always available, first
+         * implement the functionality in the {@link android.app.Activity} that starts when a user
+         * clicks the  notification (see {@link #setContentIntent setContentIntent()}), and then
+         * enhance the notification by implementing the same functionality with
+         * {@link #addAction addAction()}.
+         *
+         * @param icon Resource ID of a drawable that represents the action.
+         * @param title Text describing the action.
+         * @param intent {@link android.app.PendingIntent} to be fired when the action is invoked.
+         */
+        public Builder addAction(int icon, CharSequence title, PendingIntent intent) {
+            mActions.add(new Action(icon, title, intent));
+            return this;
+        }
+
+        /**
+         * Add a rich notification style to be applied at build time.
+         * <br>
+         * If the platform does not provide rich notification styles, this method has no effect. The
+         * user will always see the normal notification style.
+         *
+         * @param style Object responsible for modifying the notification style.
+         */
+        public Builder setStyle(Style style) {
+            if (mStyle != style) {
+                mStyle = style;
+                if (mStyle != null) {
+                    mStyle.setBuilder(this);
+                }
+            }
+            return this;
+        }
+
+        /**
+         * @deprecated Use {@link #build()} instead.
+         */
+        @Deprecated
+        public Notification getNotification() {
+            return IMPL.build(this);
+        }
+
+        /**
+         * Combine all of the options that have been set and return a new {@link Notification}
+         * object.
+         */
+        public Notification build() {
+            return IMPL.build(this);
+        }
+    }
+
+    /**
+     * An object that can apply a rich notification style to a {@link Notification.Builder}
+     * object.
+     * <br>
+     * If the platform does not provide rich notification styles, methods in this class have no
+     * effect.
+     */
+    public static abstract class Style
+    {
+        Builder mBuilder;
+        CharSequence mBigContentTitle;
+        CharSequence mSummaryText;
+        boolean mSummaryTextSet = false;
+
+        public void setBuilder(Builder builder) {
+            if (mBuilder != builder) {
+                mBuilder = builder;
+                if (mBuilder != null) {
+                    mBuilder.setStyle(this);
+                }
+            }
+        }
+
+        public Notification build() {
+            Notification notification = null;
+            if (mBuilder != null) {
+                notification = mBuilder.build();
+            }
+            return notification;
+        }
+    }
+
+    /**
+     * Helper class for generating large-format notifications that include a large image attachment.
+     * <br>
+     * If the platform does not provide large-format notifications, this method has no effect. The
+     * user will always see the normal notification view.
+     * <br>
+     * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so:
+     * <pre class="prettyprint">
+     * Notification noti = new Notification.Builder()
+     *     .setContentTitle(&quot;New photo from &quot; + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_post)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.BigPictureStyle()
+     *         .bigPicture(aBigBitmap))
+     *     .build();
+     * </pre>
+     *
+     * @see Notification#bigContentView
+     */
+    public static class BigPictureStyle extends Style {
+        Bitmap mPicture;
+
+        public BigPictureStyle() {
+        }
+
+        public BigPictureStyle(Builder builder) {
+            setBuilder(builder);
+        }
+
+        /**
+         * Overrides ContentTitle in the big form of the template.
+         * This defaults to the value passed to setContentTitle().
+         */
+        public BigPictureStyle setBigContentTitle(CharSequence title) {
+            mBigContentTitle = title;
+            return this;
+        }
+
+        /**
+         * Set the first line of text after the detail section in the big form of the template.
+         */
+        public BigPictureStyle setSummaryText(CharSequence cs) {
+            mSummaryText = cs;
+            mSummaryTextSet = true;
+            return this;
+        }
+
+        /**
+         * Provide the bitmap to be used as the payload for the BigPicture notification.
+         */
+        public BigPictureStyle bigPicture(Bitmap b) {
+            mPicture = b;
+            return this;
+        }
+    }
+
+    /**
+     * Helper class for generating large-format notifications that include a lot of text.
+     *
+     * <br>
+     * If the platform does not provide large-format notifications, this method has no effect. The
+     * user will always see the normal notification view.
+     * <br>
+     * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so:
+     * <pre class="prettyprint">
+     * Notification noti = new Notification.Builder()
+     *     .setContentTitle(&quot;New mail from &quot; + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_mail)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.BigTextStyle()
+     *         .bigText(aVeryLongString))
+     *     .build();
+     * </pre>
+     *
+     * @see Notification#bigContentView
+     */
+    public static class BigTextStyle extends Style {
+        CharSequence mBigText;
+
+        public BigTextStyle() {
+        }
+
+        public BigTextStyle(Builder builder) {
+            setBuilder(builder);
+        }
+
+        /**
+         * Overrides ContentTitle in the big form of the template.
+         * This defaults to the value passed to setContentTitle().
+         */
+        public BigTextStyle setBigContentTitle(CharSequence title) {
+            mBigContentTitle = title;
+            return this;
+        }
+
+        /**
+         * Set the first line of text after the detail section in the big form of the template.
+         */
+        public BigTextStyle setSummaryText(CharSequence cs) {
+            mSummaryText = cs;
+            mSummaryTextSet = true;
+            return this;
+        }
+
+        /**
+         * Provide the longer text to be displayed in the big form of the
+         * template in place of the content text.
+         */
+        public BigTextStyle bigText(CharSequence cs) {
+            mBigText = cs;
+            return this;
+        }
+    }
+
+    /**
+     * Helper class for generating large-format notifications that include a list of (up to 5) strings.
+     *
+     * <br>
+     * If the platform does not provide large-format notifications, this method has no effect. The
+     * user will always see the normal notification view.
+     * <br>
+     * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so:
+     * <pre class="prettyprint">
+     * Notification noti = new Notification.Builder()
+     *     .setContentTitle(&quot;5 New mails from &quot; + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_mail)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.InboxStyle()
+     *         .addLine(str1)
+     *         .addLine(str2)
+     *         .setContentTitle(&quot;&quot;)
+     *         .setSummaryText(&quot;+3 more&quot;))
+     *     .build();
+     * </pre>
+     *
+     * @see Notification#bigContentView
+     */
+    public static class InboxStyle extends Style {
+        ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
+
+        public InboxStyle() {
+        }
+
+        public InboxStyle(Builder builder) {
+            setBuilder(builder);
+        }
+
+        /**
+         * Overrides ContentTitle in the big form of the template.
+         * This defaults to the value passed to setContentTitle().
+         */
+        public InboxStyle setBigContentTitle(CharSequence title) {
+            mBigContentTitle = title;
+            return this;
+        }
+
+        /**
+         * Set the first line of text after the detail section in the big form of the template.
+         */
+        public InboxStyle setSummaryText(CharSequence cs) {
+            mSummaryText = cs;
+            mSummaryTextSet = true;
+            return this;
+        }
+
+        /**
+         * Append a line to the digest section of the Inbox notification.
+         */
+        public InboxStyle addLine(CharSequence cs) {
+            mTexts.add(cs);
+            return this;
+        }
+    }
+
+    public static class Action {
+        public int icon;
+        public CharSequence title;
+        public PendingIntent actionIntent;
+
+        public Action(int icon_, CharSequence title_, PendingIntent intent_) {
+            this.icon = icon_;
+            this.title = title_;
+            this.actionIntent = intent_;
+        }
+    }
+}
diff --git a/lib/android/support/v4/app/NotificationCompatHoneycomb.java b/lib/android/support/v4/app/NotificationCompatHoneycomb.java
new file mode 100644 (file)
index 0000000..5530ecd
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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 android.support.v4.app;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.widget.RemoteViews;
+
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+class NotificationCompatHoneycomb {
+    @SuppressWarnings("deprecation")
+       static Notification add(Context context, Notification n,
+            CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
+            RemoteViews tickerView, int number,
+            PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon) {
+        Notification.Builder b = new Notification.Builder(context)
+                .setWhen(n.when)
+                .setSmallIcon(n.icon, n.iconLevel)
+                .setContent(n.contentView)
+                .setTicker(n.tickerText, tickerView)
+                .setSound(n.sound, n.audioStreamType)
+                .setVibrate(n.vibrate)
+                .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
+                .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
+                .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)
+                .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0)
+                .setDefaults(n.defaults)
+                .setContentTitle(contentTitle)
+                .setContentText(contentText)
+                .setContentInfo(contentInfo)
+                .setContentIntent(contentIntent)
+                .setDeleteIntent(n.deleteIntent)
+                .setFullScreenIntent(fullScreenIntent,
+                        (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
+                .setLargeIcon(largeIcon)
+                .setNumber(number);
+
+        return b.getNotification();
+    }
+}
diff --git a/lib/android/support/v4/app/NotificationCompatIceCreamSandwich.java b/lib/android/support/v4/app/NotificationCompatIceCreamSandwich.java
new file mode 100644 (file)
index 0000000..f3f8cb4
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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 android.support.v4.app;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.widget.RemoteViews;
+
+@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+class NotificationCompatIceCreamSandwich {
+    @SuppressWarnings("deprecation")
+       static Notification add(Context context, Notification n,
+            CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
+            RemoteViews tickerView, int number,
+            PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
+            int mProgressMax, int mProgress, boolean mProgressIndeterminate) {
+        Notification.Builder b = new Notification.Builder(context)
+                .setWhen(n.when)
+                .setSmallIcon(n.icon, n.iconLevel)
+                .setContent(n.contentView)
+                .setTicker(n.tickerText, tickerView)
+                .setSound(n.sound, n.audioStreamType)
+                .setVibrate(n.vibrate)
+                .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
+                .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
+                .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)
+                .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0)
+                .setDefaults(n.defaults)
+                .setContentTitle(contentTitle)
+                .setContentText(contentText)
+                .setContentInfo(contentInfo)
+                .setContentIntent(contentIntent)
+                .setDeleteIntent(n.deleteIntent)
+                .setFullScreenIntent(fullScreenIntent,
+                        (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
+                .setLargeIcon(largeIcon)
+                .setNumber(number)
+                .setProgress(mProgressMax, mProgress, mProgressIndeterminate);
+
+        return b.getNotification();
+    }
+}
diff --git a/lib/android/support/v4/app/NotificationCompatJellybean.java b/lib/android/support/v4/app/NotificationCompatJellybean.java
new file mode 100644 (file)
index 0000000..6cc941f
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * 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 android.support.v4.app;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.widget.RemoteViews;
+import java.util.ArrayList;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+class NotificationCompatJellybean {
+    private Notification.Builder b;
+    @SuppressWarnings("deprecation")
+       public NotificationCompatJellybean(Context context, Notification n,
+            CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
+            RemoteViews tickerView, int number,
+            PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
+            int mProgressMax, int mProgress, boolean mProgressIndeterminate,
+            boolean useChronometer, int priority, CharSequence subText) {
+        b = new Notification.Builder(context)
+            .setWhen(n.when)
+            .setSmallIcon(n.icon, n.iconLevel)
+            .setContent(n.contentView)
+            .setTicker(n.tickerText, tickerView)
+            .setSound(n.sound, n.audioStreamType)
+            .setVibrate(n.vibrate)
+            .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
+            .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
+            .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)
+            .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0)
+            .setDefaults(n.defaults)
+            .setContentTitle(contentTitle)
+            .setContentText(contentText)
+            .setSubText(subText)
+            .setContentInfo(contentInfo)
+            .setContentIntent(contentIntent)
+            .setDeleteIntent(n.deleteIntent)
+            .setFullScreenIntent(fullScreenIntent,
+                    (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
+            .setLargeIcon(largeIcon)
+            .setNumber(number)
+            .setUsesChronometer(useChronometer)
+            .setPriority(priority)
+            .setProgress(mProgressMax, mProgress, mProgressIndeterminate);
+    }
+
+    public void addAction(int icon, CharSequence title, PendingIntent intent) {
+        b.addAction(icon, title, intent);
+    }
+
+    public void addBigTextStyle(CharSequence bigContentTitle, boolean useSummary,
+            CharSequence summaryText, CharSequence bigText) {
+        Notification.BigTextStyle style = new Notification.BigTextStyle(b)
+            .setBigContentTitle(bigContentTitle)
+            .bigText(bigText);
+        if (useSummary) {
+            style.setSummaryText(summaryText);
+         }
+    }
+
+    public void addBigPictureStyle(CharSequence bigContentTitle, boolean useSummary,
+            CharSequence summaryText, Bitmap bigPicture) {
+       Notification.BigPictureStyle style = new Notification.BigPictureStyle(b)
+           .setBigContentTitle(bigContentTitle)
+           .bigPicture(bigPicture);
+        if (useSummary) {
+            style.setSummaryText(summaryText);
+         }
+    }
+
+    public void addInboxStyle(CharSequence bigContentTitle, boolean useSummary,
+            CharSequence summaryText, ArrayList<CharSequence> texts) {
+        Notification.InboxStyle style = new Notification.InboxStyle(b)
+            .setBigContentTitle(bigContentTitle);
+        if (useSummary) {
+            style.setSummaryText(summaryText);
+        }
+        for (CharSequence text: texts) {
+            style.addLine(text);
+        }
+    }
+
+    public Notification build() {
+        return b.build();
+    }
+}
diff --git a/lib/android/support/v4/content/LocalBroadcastManager.java b/lib/android/support/v4/content/LocalBroadcastManager.java
new file mode 100644 (file)
index 0000000..7a7c50f
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2011 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 android.support.v4.content;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * Helper to register for and send broadcasts of Intents to local objects
+ * within your process.  This is has a number of advantages over sending
+ * global broadcasts with {@link android.content.Context#sendBroadcast}:
+ * <ul>
+ * <li> You know that the data you are broadcasting won't leave your app, so
+ * don't need to worry about leaking private data.
+ * <li> It is not possible for other applications to send these broadcasts to
+ * your app, so you don't need to worry about having security holes they can
+ * exploit.
+ * <li> It is more efficient than sending a global broadcast through the
+ * system.
+ * </ul>
+ */
+public class LocalBroadcastManager {
+    private static class ReceiverRecord {
+        final IntentFilter filter;
+        final BroadcastReceiver receiver;
+        boolean broadcasting;
+
+        ReceiverRecord(IntentFilter _filter, BroadcastReceiver _receiver) {
+            filter = _filter;
+            receiver = _receiver;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder(128);
+            builder.append("Receiver{");
+            builder.append(receiver);
+            builder.append(" filter=");
+            builder.append(filter);
+            builder.append("}");
+            return builder.toString();
+        }
+    }
+
+    private static class BroadcastRecord {
+        final Intent intent;
+        final ArrayList<ReceiverRecord> receivers;
+
+        BroadcastRecord(Intent _intent, ArrayList<ReceiverRecord> _receivers) {
+            intent = _intent;
+            receivers = _receivers;
+        }
+    }
+
+    private static final String TAG = "LocalBroadcastManager";
+    private static final boolean DEBUG = false;
+
+    private final Context mAppContext;
+
+    private final HashMap<BroadcastReceiver, ArrayList<IntentFilter>> mReceivers
+            = new HashMap<BroadcastReceiver, ArrayList<IntentFilter>>();
+    private final HashMap<String, ArrayList<ReceiverRecord>> mActions
+            = new HashMap<String, ArrayList<ReceiverRecord>>();
+
+    private final ArrayList<BroadcastRecord> mPendingBroadcasts
+            = new ArrayList<BroadcastRecord>();
+
+    static final int MSG_EXEC_PENDING_BROADCASTS = 1;
+
+    private final Handler mHandler;
+
+    private static final Object mLock = new Object();
+    private static LocalBroadcastManager mInstance;
+
+    public static LocalBroadcastManager getInstance(Context context) {
+        synchronized (mLock) {
+            if (mInstance == null) {
+                mInstance = new LocalBroadcastManager(context.getApplicationContext());
+            }
+            return mInstance;
+        }
+    }
+
+    private LocalBroadcastManager(Context context) {
+        mAppContext = context;
+        mHandler = new Handler(context.getMainLooper()) {
+
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_EXEC_PENDING_BROADCASTS:
+                        executePendingBroadcasts();
+                        break;
+                    default:
+                        super.handleMessage(msg);
+                }
+            }
+        };
+    }
+
+    /**
+     * Register a receive for any local broadcasts that match the given IntentFilter.
+     *
+     * @param receiver The BroadcastReceiver to handle the broadcast.
+     * @param filter Selects the Intent broadcasts to be received.
+     *
+     * @see #unregisterReceiver
+     */
+    public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+        synchronized (mReceivers) {
+            ReceiverRecord entry = new ReceiverRecord(filter, receiver);
+            ArrayList<IntentFilter> filters = mReceivers.get(receiver);
+            if (filters == null) {
+                filters = new ArrayList<IntentFilter>(1);
+                mReceivers.put(receiver, filters);
+            }
+            filters.add(filter);
+            for (int i=0; i<filter.countActions(); i++) {
+                String action = filter.getAction(i);
+                ArrayList<ReceiverRecord> entries = mActions.get(action);
+                if (entries == null) {
+                    entries = new ArrayList<ReceiverRecord>(1);
+                    mActions.put(action, entries);
+                }
+                entries.add(entry);
+            }
+        }
+    }
+
+    /**
+     * Unregister a previously registered BroadcastReceiver.  <em>All</em>
+     * filters that have been registered for this BroadcastReceiver will be
+     * removed.
+     *
+     * @param receiver The BroadcastReceiver to unregister.
+     *
+     * @see #registerReceiver
+     */
+    public void unregisterReceiver(BroadcastReceiver receiver) {
+        synchronized (mReceivers) {
+            ArrayList<IntentFilter> filters = mReceivers.remove(receiver);
+            if (filters == null) {
+                return;
+            }
+            for (int i=0; i<filters.size(); i++) {
+                IntentFilter filter = filters.get(i);
+                for (int j=0; j<filter.countActions(); j++) {
+                    String action = filter.getAction(j);
+                    ArrayList<ReceiverRecord> receivers = mActions.get(action);
+                    if (receivers != null) {
+                        for (int k=0; k<receivers.size(); k++) {
+                            if (receivers.get(k).receiver == receiver) {
+                                receivers.remove(k);
+                                k--;
+                            }
+                        }
+                        if (receivers.size() <= 0) {
+                            mActions.remove(action);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Broadcast the given intent to all interested BroadcastReceivers.  This
+     * call is asynchronous; it returns immediately, and you will continue
+     * executing while the receivers are run.
+     *
+     * @param intent The Intent to broadcast; all receivers matching this
+     *     Intent will receive the broadcast.
+     *
+     * @see #registerReceiver
+     */
+    public boolean sendBroadcast(Intent intent) {
+        synchronized (mReceivers) {
+            final String action = intent.getAction();
+            final String type = intent.resolveTypeIfNeeded(
+                    mAppContext.getContentResolver());
+            final Uri data = intent.getData();
+            final String scheme = intent.getScheme();
+            final Set<String> categories = intent.getCategories();
+
+            final boolean debug = DEBUG ||
+                    ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0);
+            if (debug) Log.v(
+                    TAG, "Resolving type " + type + " scheme " + scheme
+                    + " of intent " + intent);
+
+            ArrayList<ReceiverRecord> entries = mActions.get(intent.getAction());
+            if (entries != null) {
+                if (debug) Log.v(TAG, "Action list: " + entries);
+
+                ArrayList<ReceiverRecord> receivers = null;
+                for (int i=0; i<entries.size(); i++) {
+                    ReceiverRecord receiver = entries.get(i);
+                    if (debug) Log.v(TAG, "Matching against filter " + receiver.filter);
+
+                    if (receiver.broadcasting) {
+                        if (debug) {
+                            Log.v(TAG, "  Filter's target already added");
+                        }
+                        continue;
+                    }
+
+                    int match = receiver.filter.match(action, type, scheme, data,
+                            categories, "LocalBroadcastManager");
+                    if (match >= 0) {
+                        if (debug) Log.v(TAG, "  Filter matched!  match=0x" +
+                                Integer.toHexString(match));
+                        if (receivers == null) {
+                            receivers = new ArrayList<ReceiverRecord>();
+                        }
+                        receivers.add(receiver);
+                        receiver.broadcasting = true;
+                    } else {
+                        if (debug) {
+                            String reason;
+                            switch (match) {
+                                case IntentFilter.NO_MATCH_ACTION: reason = "action"; break;
+                                case IntentFilter.NO_MATCH_CATEGORY: reason = "category"; break;
+                                case IntentFilter.NO_MATCH_DATA: reason = "data"; break;
+                                case IntentFilter.NO_MATCH_TYPE: reason = "type"; break;
+                                default: reason = "unknown reason"; break;
+                            }
+                            Log.v(TAG, "  Filter did not match: " + reason);
+                        }
+                    }
+                }
+
+                if (receivers != null) {
+                    for (int i=0; i<receivers.size(); i++) {
+                        receivers.get(i).broadcasting = false;
+                    }
+                    mPendingBroadcasts.add(new BroadcastRecord(intent, receivers));
+                    if (!mHandler.hasMessages(MSG_EXEC_PENDING_BROADCASTS)) {
+                        mHandler.sendEmptyMessage(MSG_EXEC_PENDING_BROADCASTS);
+                    }
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Like {@link #sendBroadcast(Intent)}, but if there are any receivers for
+     * the Intent this function will block and immediately dispatch them before
+     * returning.
+     */
+    public void sendBroadcastSync(Intent intent) {
+        if (sendBroadcast(intent)) {
+            executePendingBroadcasts();
+        }
+    }
+
+    private void executePendingBroadcasts() {
+        while (true) {
+            BroadcastRecord[] brs = null;
+            synchronized (mReceivers) {
+                final int N = mPendingBroadcasts.size();
+                if (N <= 0) {
+                    return;
+                }
+                brs = new BroadcastRecord[N];
+                mPendingBroadcasts.toArray(brs);
+                mPendingBroadcasts.clear();
+            }
+            for (int i=0; i<brs.length; i++) {
+                BroadcastRecord br = brs[i];
+                for (int j=0; j<br.receivers.size(); j++) {
+                    br.receivers.get(j).receiver.onReceive(mAppContext, br.intent);
+                }
+            }
+        }
+    }
+}
diff --git a/lib/com/google/android/gcm/GCMBaseIntentService.java b/lib/com/google/android/gcm/GCMBaseIntentService.java
new file mode 100644 (file)
index 0000000..ab1e489
--- /dev/null
@@ -0,0 +1,345 @@
+/*
+ * Copyright 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.google.android.gcm;
+
+import static com.google.android.gcm.GCMConstants.ERROR_SERVICE_NOT_AVAILABLE;
+import static com.google.android.gcm.GCMConstants.EXTRA_ERROR;
+import static com.google.android.gcm.GCMConstants.EXTRA_REGISTRATION_ID;
+import static com.google.android.gcm.GCMConstants.EXTRA_SPECIAL_MESSAGE;
+import static com.google.android.gcm.GCMConstants.EXTRA_TOTAL_DELETED;
+import static com.google.android.gcm.GCMConstants.EXTRA_UNREGISTERED;
+import static com.google.android.gcm.GCMConstants.INTENT_FROM_GCM_LIBRARY_RETRY;
+import static com.google.android.gcm.GCMConstants.INTENT_FROM_GCM_MESSAGE;
+import static com.google.android.gcm.GCMConstants.INTENT_FROM_GCM_REGISTRATION_CALLBACK;
+import static com.google.android.gcm.GCMConstants.VALUE_DELETED_MESSAGES;
+
+import android.app.AlarmManager;
+import android.app.IntentService;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Skeleton for application-specific {@link IntentService}s responsible for
+ * handling communication from Google Cloud Messaging service.
+ * <p>
+ * The abstract methods in this class are called from its worker thread, and
+ * hence should run in a limited amount of time. If they execute long
+ * operations, they should spawn new threads, otherwise the worker thread will
+ * be blocked.
+ * <p>
+ * Subclasses must provide a public no-arg constructor.
+ */
+public abstract class GCMBaseIntentService extends IntentService {
+
+    public static final String TAG = "GCMBaseIntentService";
+
+    // wakelock
+    private static final String WAKELOCK_KEY = "GCM_LIB";
+    private static PowerManager.WakeLock sWakeLock;
+
+    // Java lock used to synchronize access to sWakelock
+    private static final Object LOCK = GCMBaseIntentService.class;
+
+    private final String[] mSenderIds;
+
+    // instance counter
+    private static int sCounter = 0;
+
+    private static final Random sRandom = new Random();
+
+    private static final int MAX_BACKOFF_MS =
+        (int) TimeUnit.SECONDS.toMillis(3600); // 1 hour
+
+    // token used to check intent origin
+    private static final String TOKEN =
+            Long.toBinaryString(sRandom.nextLong());
+    private static final String EXTRA_TOKEN = "token";
+
+    /**
+     * Constructor that does not set a sender id, useful when the sender id
+     * is context-specific.
+     * <p>
+     * When using this constructor, the subclass <strong>must</strong>
+     * override {@link #getSenderIds(Context)}, otherwise methods such as
+     * {@link #onHandleIntent(Intent)} will throw an
+     * {@link IllegalStateException} on runtime.
+     */
+    protected GCMBaseIntentService() {
+        this(getName("DynamicSenderIds"), null);
+    }
+
+    /**
+     * Constructor used when the sender id(s) is fixed.
+     */
+    protected GCMBaseIntentService(String... senderIds) {
+        this(getName(senderIds), senderIds);
+    }
+
+    private GCMBaseIntentService(String name, String[] senderIds) {
+        super(name);  // name is used as base name for threads, etc.
+        mSenderIds = senderIds;
+    }
+
+    private static String getName(String senderId) {
+        String name = "GCMIntentService-" + senderId + "-" + (++sCounter);
+        Log.v(TAG, "Intent service name: " + name);
+        return name;
+    }
+
+    private static String getName(String[] senderIds) {
+        String flatSenderIds = GCMRegistrar.getFlatSenderIds(senderIds);
+        return getName(flatSenderIds);
+    }
+
+    /**
+     * Gets the sender ids.
+     *
+     * <p>By default, it returns the sender ids passed in the constructor, but
+     * it could be overridden to provide a dynamic sender id.
+     *
+     * @throws IllegalStateException if sender id was not set on constructor.
+     */
+    protected String[] getSenderIds() {
+        if (mSenderIds == null) {
+            throw new IllegalStateException("sender id not set on constructor");
+        }
+        return mSenderIds;
+    }
+
+    /**
+     * Called when a cloud message has been received.
+     *
+     * @param context application's context.
+     * @param intent intent containing the message payload as extras.
+     */
+    protected abstract void onMessage(Context context, Intent intent);
+
+    /**
+     * Called when the GCM server tells pending messages have been deleted
+     * because the device was idle.
+     *
+     * @param context application's context.
+     * @param total total number of collapsed messages
+     */
+    protected void onDeletedMessages(Context context, int total) {
+       //do nothing
+    }
+
+    /**
+     * Called on a registration error that could be retried.
+     *
+     * <p>By default, it does nothing and returns {@literal true}, but could be
+     * overridden to change that behavior and/or display the error.
+     *
+     * @param context application's context.
+     * @param errorId error id returned by the GCM service.
+     *
+     * @return if {@literal true}, failed operation will be retried (using
+     *         exponential backoff).
+     */
+    protected static boolean onRecoverableError(Context context, String errorId) {
+        return true;
+    }
+
+    /**
+     * Called on registration or unregistration error.
+     *
+     * @param context application's context.
+     * @param errorId error id returned by the GCM service.
+     */
+    protected abstract void onError(Context context, String errorId);
+
+    /**
+     * Called after a device has been registered.
+     *
+     * @param context application's context.
+     * @param registrationId the registration id returned by the GCM service.
+     */
+    protected abstract void onRegistered(Context context,
+            String registrationId);
+
+    /**
+     * Called after a device has been unregistered.
+     *
+     * @param registrationId the registration id that was previously registered.
+     * @param context application's context.
+     */
+    protected abstract void onUnregistered(Context context,
+            String registrationId);
+
+    @Override
+    public final void onHandleIntent(Intent intent) {
+        try {
+            Context context = getApplicationContext();
+            String action = intent.getAction();
+            if (action.equals(INTENT_FROM_GCM_REGISTRATION_CALLBACK)) {
+                GCMRegistrar.setRetryBroadcastReceiver(context);
+                handleRegistration(context, intent);
+            } else if (action.equals(INTENT_FROM_GCM_MESSAGE)) {
+                // checks for special messages
+                String messageType =
+                        intent.getStringExtra(EXTRA_SPECIAL_MESSAGE);
+                if (messageType != null) {
+                    if (messageType.equals(VALUE_DELETED_MESSAGES)) {
+                        String sTotal =
+                                intent.getStringExtra(EXTRA_TOTAL_DELETED);
+                        if (sTotal != null) {
+                            try {
+                                int total = Integer.parseInt(sTotal);
+                                Log.v(TAG, "Received deleted messages " +
+                                        "notification: " + total);
+                                onDeletedMessages(context, total);
+                            } catch (NumberFormatException e) {
+                                Log.e(TAG, "GCM returned invalid number of " +
+                                        "deleted messages: " + sTotal);
+                            }
+                        }
+                    } else {
+                        // application is not using the latest GCM library
+                        Log.e(TAG, "Received unknown special message: " +
+                                messageType);
+                    }
+                } else {
+                    onMessage(context, intent);
+                }
+            } else if (action.equals(INTENT_FROM_GCM_LIBRARY_RETRY)) {
+                String token = intent.getStringExtra(EXTRA_TOKEN);
+                if (!TOKEN.equals(token)) {
+                    // make sure intent was generated by this class, not by a
+                    // malicious app.
+                    Log.e(TAG, "Received invalid token: " + token);
+                    return;
+                }
+                // retry last call
+                if (GCMRegistrar.isRegistered(context)) {
+                    GCMRegistrar.internalUnregister(context);
+                } else {
+                    String[] senderIds = getSenderIds();
+                    GCMRegistrar.internalRegister(context, senderIds);
+                }
+            }
+        } finally {
+            // Release the power lock, so phone can get back to sleep.
+            // The lock is reference-counted by default, so multiple
+            // messages are ok.
+
+            // If onMessage() needs to spawn a thread or do something else,
+            // it should use its own lock.
+            synchronized (LOCK) {
+                // sanity check for null as this is a public method
+                if (sWakeLock != null) {
+                    Log.v(TAG, "Releasing wakelock");
+                    sWakeLock.release();
+                } else {
+                    // should never happen during normal workflow
+                    Log.e(TAG, "Wakelock reference is null");
+                }
+            }
+        }
+    }
+
+    /**
+     * Called from the broadcast receiver.
+     * <p>
+     * Will process the received intent, call handleMessage(), registered(),
+     * etc. in background threads, with a wake lock, while keeping the service
+     * alive.
+     */
+    static void runIntentInService(Context context, Intent intent,
+            String className) {
+        synchronized (LOCK) {
+            if (sWakeLock == null) {
+                // This is called from BroadcastReceiver, there is no init.
+                PowerManager pm = (PowerManager)
+                        context.getSystemService(Context.POWER_SERVICE);
+                sWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                        WAKELOCK_KEY);
+            }
+        }
+        Log.v(TAG, "Acquiring wakelock");
+        sWakeLock.acquire();
+        intent.setClassName(context, className);
+        context.startService(intent);
+    }
+
+    private void handleRegistration(final Context context, Intent intent) {
+        String registrationId = intent.getStringExtra(EXTRA_REGISTRATION_ID);
+        String error = intent.getStringExtra(EXTRA_ERROR);
+        String unregistered = intent.getStringExtra(EXTRA_UNREGISTERED);
+        Log.d(TAG, "handleRegistration: registrationId = " + registrationId +
+                ", error = " + error + ", unregistered = " + unregistered);
+
+        // registration succeeded
+        if (registrationId != null) {
+            GCMRegistrar.resetBackoff(context);
+            GCMRegistrar.setRegistrationId(context, registrationId);
+            onRegistered(context, registrationId);
+            return;
+        }
+
+        // unregistration succeeded
+        if (unregistered != null) {
+            // Remember we are unregistered
+            GCMRegistrar.resetBackoff(context);
+            String oldRegistrationId =
+                    GCMRegistrar.clearRegistrationId(context);
+            onUnregistered(context, oldRegistrationId);
+            return;
+        }
+
+        // last operation (registration or unregistration) returned an error;
+        Log.d(TAG, "Registration error: " + error);
+        // Registration failed
+        if (ERROR_SERVICE_NOT_AVAILABLE.equals(error)) {
+            boolean retry = onRecoverableError(context, error);
+            if (retry) {
+                int backoffTimeMs = GCMRegistrar.getBackoff(context);
+                int nextAttempt = backoffTimeMs / 2 +
+                        sRandom.nextInt(backoffTimeMs);
+                Log.d(TAG, "Scheduling registration retry, backoff = " +
+                        nextAttempt + " (" + backoffTimeMs + ")");
+                Intent retryIntent =
+                        new Intent(INTENT_FROM_GCM_LIBRARY_RETRY);
+                retryIntent.putExtra(EXTRA_TOKEN, TOKEN);
+                PendingIntent retryPendingIntent = PendingIntent
+                        .getBroadcast(context, 0, retryIntent, 0);
+                AlarmManager am = (AlarmManager)
+                        context.getSystemService(Context.ALARM_SERVICE);
+                am.set(AlarmManager.ELAPSED_REALTIME,
+                        SystemClock.elapsedRealtime() + nextAttempt,
+                        retryPendingIntent);
+                // Next retry should wait longer.
+                if (backoffTimeMs < MAX_BACKOFF_MS) {
+                  GCMRegistrar.setBackoff(context, backoffTimeMs * 2);
+                }
+            } else {
+                Log.d(TAG, "Not retrying failed operation");
+            }
+        } else {
+            // Unrecoverable error, notify app
+            onError(context, error);
+        }
+    }
+
+}
diff --git a/lib/com/google/android/gcm/GCMBroadcastReceiver.java b/lib/com/google/android/gcm/GCMBroadcastReceiver.java
new file mode 100644 (file)
index 0000000..bbe7b66
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright 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.google.android.gcm;
+
+import static com.google.android.gcm.GCMConstants.DEFAULT_INTENT_SERVICE_CLASS_NAME;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * {@link BroadcastReceiver} that receives GCM messages and delivers them to
+ * an application-specific {@link GCMBaseIntentService} subclass.
+ * <p>
+ * By default, the {@link GCMBaseIntentService} class belongs to the application
+ * main package and is named
+ * {@link GCMConstants#DEFAULT_INTENT_SERVICE_CLASS_NAME}. To use a new class,
+ * the {@link #getGCMIntentServiceClassName(Context)} must be overridden.
+ */
+public final class GCMBroadcastReceiver extends BroadcastReceiver {
+
+    private static final String TAG = "GCMBroadcastReceiver";
+    private static boolean mReceiverSet = false;
+
+    @Override
+    public final void onReceive(Context context, Intent intent) {
+        Log.v(TAG, "onReceive: " + intent.getAction());
+        // do a one-time check if app is using a custom GCMBroadcastReceiver
+        if (!mReceiverSet) {
+            mReceiverSet = true;
+            String myClass = getClass().getName();
+            if (!myClass.equals(GCMBroadcastReceiver.class.getName())) {
+                GCMRegistrar.setRetryReceiverClassName(myClass);
+            }
+        }
+        String className = getGCMIntentServiceClassName(context);
+        Log.v(TAG, "GCM IntentService class: " + className);
+        // Delegates to the application-specific intent service.
+        GCMBaseIntentService.runIntentInService(context, intent, className);
+        setResult(Activity.RESULT_OK, null /* data */, null /* extra */);
+    }
+
+    /**
+     * Gets the class name of the intent service that will handle GCM messages.
+     */
+    protected static String getGCMIntentServiceClassName(final Context context) {
+        return getDefaultIntentServiceClassName(context);
+    }
+
+    /**
+     * Gets the default class name of the intent service that will handle GCM
+     * messages.
+     */
+    private static final String getDefaultIntentServiceClassName(final Context context) {
+        String className = context.getPackageName() +
+                DEFAULT_INTENT_SERVICE_CLASS_NAME;
+        return className;
+    }
+}
diff --git a/lib/com/google/android/gcm/GCMConstants.java b/lib/com/google/android/gcm/GCMConstants.java
new file mode 100644 (file)
index 0000000..abcdcfb
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * Copyright 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.google.android.gcm;
+
+/**
+ * Constants used by the GCM library.
+ */
+public final class GCMConstants {
+
+    /**
+     * Intent sent to GCM to register the application.
+     */
+    public static final String INTENT_TO_GCM_REGISTRATION =
+            "com.google.android.c2dm.intent.REGISTER";
+
+    /**
+     * Intent sent to GCM to unregister the application.
+     */
+    public static final String INTENT_TO_GCM_UNREGISTRATION =
+            "com.google.android.c2dm.intent.UNREGISTER";
+
+    /**
+     * Intent sent by GCM indicating with the result of a registration request.
+     */
+    public static final String INTENT_FROM_GCM_REGISTRATION_CALLBACK =
+            "com.google.android.c2dm.intent.REGISTRATION";
+
+    /**
+     * Intent used by the GCM library to indicate that the registration call
+     * should be retried.
+     */
+    public static final String INTENT_FROM_GCM_LIBRARY_RETRY =
+            "com.google.android.gcm.intent.RETRY";
+
+    /**
+     * Intent sent by GCM containing a message.
+     */
+    public static final String INTENT_FROM_GCM_MESSAGE =
+            "com.google.android.c2dm.intent.RECEIVE";
+
+    /**
+     * Extra used on {@link #INTENT_TO_GCM_REGISTRATION} to indicate the sender
+     * account (a Google email) that owns the application.
+     */
+    public static final String EXTRA_SENDER = "sender";
+
+    /**
+     * Extra used on {@link #INTENT_TO_GCM_REGISTRATION} to get the application
+     * id.
+     */
+    public static final String EXTRA_APPLICATION_PENDING_INTENT = "app";
+
+    /**
+     * Extra used on {@link #INTENT_FROM_GCM_REGISTRATION_CALLBACK} to indicate
+     * that the application has been unregistered.
+     */
+    public static final String EXTRA_UNREGISTERED = "unregistered";
+
+    /**
+     * Extra used on {@link #INTENT_FROM_GCM_REGISTRATION_CALLBACK} to indicate
+     * an error when the registration fails. See constants starting with ERROR_
+     * for possible values.
+     */
+    public static final String EXTRA_ERROR = "error";
+
+    /**
+     * Extra used on {@link #INTENT_FROM_GCM_REGISTRATION_CALLBACK} to indicate
+     * the registration id when the registration succeeds.
+     */
+    public static final String EXTRA_REGISTRATION_ID = "registration_id";
+
+    /**
+     * Type of message present in the {@link #INTENT_FROM_GCM_MESSAGE} intent.
+     * This extra is only set for special messages sent from GCM, not for
+     * messages originated from the application.
+     */
+    public static final String EXTRA_SPECIAL_MESSAGE = "message_type";
+
+    /**
+     * Special message indicating the server deleted the pending messages.
+     */
+    public static final String VALUE_DELETED_MESSAGES = "deleted_messages";
+
+    /**
+     * Number of messages deleted by the server because the device was idle.
+     * Present only on messages of special type
+     * {@link #VALUE_DELETED_MESSAGES}
+     */
+    public static final String EXTRA_TOTAL_DELETED = "total_deleted";
+
+    /**
+     * Permission necessary to receive GCM intents.
+     */
+    public static final String PERMISSION_GCM_INTENTS =
+            "com.google.android.c2dm.permission.SEND";
+
+    /**
+     * @see GCMBroadcastReceiver
+     */
+    public static final String DEFAULT_INTENT_SERVICE_CLASS_NAME =
+            ".GCMIntentService";
+
+    /**
+     * The device can't read the response, or there was a 500/503 from the
+     * server that can be retried later. The application should use exponential
+     * back off and retry.
+     */
+    public static final String ERROR_SERVICE_NOT_AVAILABLE =
+            "SERVICE_NOT_AVAILABLE";
+
+    /**
+     * There is no Google account on the phone. The application should ask the
+     * user to open the account manager and add a Google account.
+     */
+    public static final String ERROR_ACCOUNT_MISSING =
+            "ACCOUNT_MISSING";
+
+    /**
+     * Bad password. The application should ask the user to enter his/her
+     * password, and let user retry manually later. Fix on the device side.
+     */
+    public static final String ERROR_AUTHENTICATION_FAILED =
+            "AUTHENTICATION_FAILED";
+
+    /**
+     * The request sent by the phone does not contain the expected parameters.
+     * This phone doesn't currently support GCM.
+     */
+    public static final String ERROR_INVALID_PARAMETERS =
+            "INVALID_PARAMETERS";
+    /**
+     * The sender account is not recognized. Fix on the device side.
+     */
+    public static final String ERROR_INVALID_SENDER =
+            "INVALID_SENDER";
+
+    /**
+     * Incorrect phone registration with Google. This phone doesn't currently
+     * support GCM.
+     */
+    public static final String ERROR_PHONE_REGISTRATION_ERROR =
+            "PHONE_REGISTRATION_ERROR";
+
+    private GCMConstants() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/lib/com/google/android/gcm/GCMRegistrar.java b/lib/com/google/android/gcm/GCMRegistrar.java
new file mode 100644 (file)
index 0000000..0bd5c9f
--- /dev/null
@@ -0,0 +1,316 @@
+/*
+ * Copyright 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.google.android.gcm;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Build;
+import android.util.Log;
+
+/**
+ * Utilities for device registration.
+ * <p>
+ * <strong>Note:</strong> this class uses a private {@link SharedPreferences}
+ * object to keep track of the registration token.
+ */
+public final class GCMRegistrar {
+
+    /**
+     * Default lifespan (7 days) of the {@link #isRegisteredOnServer(Context)}
+     * flag until it is considered expired.
+     */
+    // NOTE: cannot use TimeUnit.DAYS because it's not available on API Level 8
+    public static final long DEFAULT_ON_SERVER_LIFESPAN_MS =
+            1000 * 3600 * 24 * 7;
+
+    private static final String TAG = "GCMRegistrar";
+    private static final String BACKOFF_MS = "backoff_ms";
+    private static final String GSF_PACKAGE = "com.google.android.gsf";
+    private static final String PREFERENCES = "com.google.android.gcm";
+    private static final int DEFAULT_BACKOFF_MS = 3000;
+    private static final String PROPERTY_REG_ID = "regId";
+    private static final String PROPERTY_APP_VERSION = "appVersion";
+    /**
+     * {@link GCMBroadcastReceiver} instance used to handle the retry intent.
+     *
+     * <p>
+     * This instance cannot be the same as the one defined in the manifest
+     * because it needs a different permission.
+     */
+    private static GCMBroadcastReceiver sRetryReceiver;
+
+    private static String sRetryReceiverClassName;
+
+    /**
+     * Checks if the device has the proper dependencies installed.
+     * <p>
+     * This method should be called when the application starts to verify that
+     * the device supports GCM.
+     *
+     * @param context application context.
+     * @throws UnsupportedOperationException if the device does not support GCM.
+     */
+    public static void checkDevice(final Context context) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version < 8) {
+            throw new UnsupportedOperationException("Device must be at least " +
+                    "API Level 8 (instead of " + version + ")");
+        }
+        final PackageManager packageManager = context.getPackageManager();
+        try {
+            packageManager.getPackageInfo(GSF_PACKAGE, 0);
+        } catch (NameNotFoundException e) {
+            throw new UnsupportedOperationException(
+                    "Device does not have package " + GSF_PACKAGE);
+        }
+    }
+
+    /**
+     * Initiate messaging registration for the current application.
+     * <p>
+     * The result will be returned as an
+     * {@link GCMConstants#INTENT_FROM_GCM_REGISTRATION_CALLBACK} intent with
+     * either a {@link GCMConstants#EXTRA_REGISTRATION_ID} or
+     * {@link GCMConstants#EXTRA_ERROR}.
+     *
+     * @param context application context.
+     * @param senderIds Google Project ID of the accounts authorized to send
+     *    messages to this application.
+     * @throws IllegalStateException if device does not have all GCM
+     *             dependencies installed.
+     */
+    public static void register(Context context, String... senderIds) {
+        GCMRegistrar.resetBackoff(context);
+        internalRegister(context, senderIds);
+    }
+
+    static void internalRegister(Context context, String... senderIds) {
+        String flatSenderIds = getFlatSenderIds(senderIds);
+        Log.v(TAG, "Registering app "  + context.getPackageName() +
+                " of senders " + flatSenderIds);
+        Intent intent = new Intent(GCMConstants.INTENT_TO_GCM_REGISTRATION);
+        intent.setPackage(GSF_PACKAGE);
+        intent.putExtra(GCMConstants.EXTRA_APPLICATION_PENDING_INTENT,
+                PendingIntent.getBroadcast(context, 0, new Intent(), 0));
+        intent.putExtra(GCMConstants.EXTRA_SENDER, flatSenderIds);
+        context.startService(intent);
+    }
+
+    static String getFlatSenderIds(String... senderIds) {
+        if (senderIds == null || senderIds.length == 0) {
+            throw new IllegalArgumentException("No senderIds");
+        }
+        StringBuilder builder = new StringBuilder(senderIds[0]);
+        for (int i = 1; i < senderIds.length; i++) {
+            builder.append(',').append(senderIds[i]);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Clear internal resources.
+     *
+     * <p>
+     * This method should be called by the main activity's {@code onDestroy()}
+     * method.
+     */
+    public static synchronized void onDestroy(Context context) {
+        if (sRetryReceiver != null) {
+            Log.v(TAG, "Unregistering receiver");
+            context.unregisterReceiver(sRetryReceiver);
+            sRetryReceiver = null;
+        }
+    }
+
+    static void internalUnregister(Context context) {
+        Log.v(TAG, "Unregistering app "  + context.getPackageName());
+        Intent intent = new Intent(GCMConstants.INTENT_TO_GCM_UNREGISTRATION);
+        intent.setPackage(GSF_PACKAGE);
+        intent.putExtra(GCMConstants.EXTRA_APPLICATION_PENDING_INTENT,
+                PendingIntent.getBroadcast(context, 0, new Intent(), 0));
+        context.startService(intent);
+    }
+
+    /**
+     * Lazy initializes the {@link GCMBroadcastReceiver} instance.
+     */
+    static synchronized void setRetryBroadcastReceiver(Context context) {
+        if (sRetryReceiver == null) {
+            if (sRetryReceiverClassName == null) {
+                // should never happen
+                Log.e(TAG, "internal error: retry receiver class not set yet");
+                sRetryReceiver = new GCMBroadcastReceiver();
+            } else {
+                Class<?> clazz;
+                try {
+                    clazz = Class.forName(sRetryReceiverClassName);
+                    sRetryReceiver = (GCMBroadcastReceiver) clazz.newInstance();
+                } catch (Exception e) {
+                    Log.e(TAG, "Could not create instance of " +
+                            sRetryReceiverClassName + ". Using " +
+                            GCMBroadcastReceiver.class.getName() +
+                            " directly.");
+                    sRetryReceiver = new GCMBroadcastReceiver();
+                }
+            }
+            String category = context.getPackageName();
+            IntentFilter filter = new IntentFilter(
+                    GCMConstants.INTENT_FROM_GCM_LIBRARY_RETRY);
+            filter.addCategory(category);
+            // must use a permission that is defined on manifest for sure
+            String permission = category + ".permission.C2D_MESSAGE";
+            Log.v(TAG, "Registering receiver");
+            context.registerReceiver(sRetryReceiver, filter, permission, null);
+        }
+    }
+
+    /**
+     * Sets the name of the retry receiver class.
+     */
+    static void setRetryReceiverClassName(String className) {
+        Log.v(TAG, "Setting the name of retry receiver class to " + className);
+        sRetryReceiverClassName = className;
+    }
+
+    /**
+     * Gets the current registration id for application on GCM service.
+     * <p>
+     * If result is empty, the registration has failed.
+     *
+     * @return registration id, or empty string if the registration is not
+     *         complete.
+     */
+    public static String getRegistrationId(Context context) {
+        final SharedPreferences prefs = getGCMPreferences(context);
+        String registrationId = prefs.getString(PROPERTY_REG_ID, "");
+        // check if app was updated; if so, it must clear registration id to
+        // avoid a race condition if GCM sends a message
+        int oldVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
+        int newVersion = getAppVersion(context);
+        if (oldVersion != Integer.MIN_VALUE && oldVersion != newVersion) {
+            Log.v(TAG, "App version changed from " + oldVersion + " to " +
+                    newVersion + "; resetting registration id");
+            clearRegistrationId(context);
+            registrationId = "";
+        }
+        return registrationId;
+    }
+
+    /**
+     * Checks whether the application was successfully registered on GCM
+     * service.
+     */
+    static boolean isRegistered(Context context) {
+        return getRegistrationId(context).length() > 0;
+    }
+
+    /**
+     * Clears the registration id in the persistence store.
+     *
+     * @param context application's context.
+     * @return old registration id.
+     */
+    static String clearRegistrationId(Context context) {
+        return setRegistrationId(context, "");
+    }
+
+    /**
+     * Sets the registration id in the persistence store.
+     *
+     * @param context application's context.
+     * @param regId registration id
+     */
+    static String setRegistrationId(Context context, String regId) {
+        final SharedPreferences prefs = getGCMPreferences(context);
+        String oldRegistrationId = prefs.getString(PROPERTY_REG_ID, "");
+        int appVersion = getAppVersion(context);
+        Log.v(TAG, "Saving regId on app version " + appVersion);
+        Editor editor = prefs.edit();
+        editor.putString(PROPERTY_REG_ID, regId);
+        editor.putInt(PROPERTY_APP_VERSION, appVersion);
+        editor.commit();
+        return oldRegistrationId;
+    }
+
+    /**
+     * Gets the application version.
+     */
+    private static int getAppVersion(Context context) {
+        try {
+            PackageInfo packageInfo = context.getPackageManager()
+                    .getPackageInfo(context.getPackageName(), 0);
+            return packageInfo.versionCode;
+        } catch (NameNotFoundException e) {
+            // should never happen
+            throw new RuntimeException("Coult not get package name: " + e);
+        }
+    }
+
+    /**
+     * Resets the backoff counter.
+     * <p>
+     * This method should be called after a GCM call succeeds.
+     *
+     * @param context application's context.
+     */
+    static void resetBackoff(Context context) {
+        Log.d(TAG, "resetting backoff for " + context.getPackageName());
+        setBackoff(context, DEFAULT_BACKOFF_MS);
+    }
+
+    /**
+     * Gets the current backoff counter.
+     *
+     * @param context application's context.
+     * @return current backoff counter, in milliseconds.
+     */
+    static int getBackoff(Context context) {
+        final SharedPreferences prefs = getGCMPreferences(context);
+        return prefs.getInt(BACKOFF_MS, DEFAULT_BACKOFF_MS);
+    }
+
+    /**
+     * Sets the backoff counter.
+     * <p>
+     * This method should be called after a GCM call fails, passing an
+     * exponential value.
+     *
+     * @param context application's context.
+     * @param backoff new backoff counter, in milliseconds.
+     */
+    static void setBackoff(Context context, int backoff) {
+        final SharedPreferences prefs = getGCMPreferences(context);
+        Editor editor = prefs.edit();
+        editor.putInt(BACKOFF_MS, backoff);
+        editor.commit();
+    }
+
+    private static SharedPreferences getGCMPreferences(Context context) {
+        return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
+    }
+
+    private GCMRegistrar() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/proguard-project.txt b/proguard-project.txt
new file mode 100644 (file)
index 0000000..5a93951
--- /dev/null
@@ -0,0 +1,21 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/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
+-dontobfuscate
+-libraryjars org.eclipse.jdt.annotation_1.0.0.dist.jar
+# 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 *;
+#}
diff --git a/project.properties b/project.properties
new file mode 100644 (file)
index 0000000..a3ee5ab
--- /dev/null
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-17
diff --git a/res/drawable-hdpi/cross.png b/res/drawable-hdpi/cross.png
new file mode 100644 (file)
index 0000000..44fae3f
Binary files /dev/null and b/res/drawable-hdpi/cross.png differ
diff --git a/res/drawable-hdpi/tick.png b/res/drawable-hdpi/tick.png
new file mode 100644 (file)
index 0000000..20068da
Binary files /dev/null and b/res/drawable-hdpi/tick.png differ
diff --git a/res/drawable-ldpi/cross.png b/res/drawable-ldpi/cross.png
new file mode 100644 (file)
index 0000000..6f00756
Binary files /dev/null and b/res/drawable-ldpi/cross.png differ
diff --git a/res/drawable-ldpi/tick.png b/res/drawable-ldpi/tick.png
new file mode 100644 (file)
index 0000000..360e423
Binary files /dev/null and b/res/drawable-ldpi/tick.png differ
diff --git a/res/drawable-mdpi/cross.png b/res/drawable-mdpi/cross.png
new file mode 100644 (file)
index 0000000..7cf518d
Binary files /dev/null and b/res/drawable-mdpi/cross.png differ
diff --git a/res/drawable-mdpi/tick.png b/res/drawable-mdpi/tick.png
new file mode 100644 (file)
index 0000000..f80d294
Binary files /dev/null and b/res/drawable-mdpi/tick.png differ
diff --git a/res/drawable-xhdpi/cross.png b/res/drawable-xhdpi/cross.png
new file mode 100644 (file)
index 0000000..e17cadd
Binary files /dev/null and b/res/drawable-xhdpi/cross.png differ
diff --git a/res/drawable-xhdpi/tick.png b/res/drawable-xhdpi/tick.png
new file mode 100644 (file)
index 0000000..3e775e0
Binary files /dev/null and b/res/drawable-xhdpi/tick.png differ
diff --git a/res/layout/local.xml b/res/layout/local.xml
new file mode 100644 (file)
index 0000000..2164b5c
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" >
+
+        <TextView
+            android:id="@+id/consoleTextView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:freezesText="true"
+            android:textIsSelectable="true" />
+
+    </ScrollView>
+
+    <EditText
+        android:id="@+id/argEditText"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:imeOptions="actionSend|flagForceAscii"
+        android:inputType="text"
+        android:singleLine="true" >
+        <requestFocus />
+    </EditText>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/main.xml b/res/layout/main.xml
new file mode 100644 (file)
index 0000000..1fc54d2
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <TextView
+        android:id="@+id/resultTextView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:textIsSelectable="false" />
+
+    <ListView
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+    </ListView>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/ongoing_list_item.xml b/res/layout/ongoing_list_item.xml
new file mode 100644 (file)
index 0000000..f71e285
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal" >
+
+    <TextView
+        android:id="@+id/ongoingItemTextView"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:textIsSelectable="false" />
+
+    <Button
+        android:id="@+id/ongoingItemButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/cancel" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/menu/main.xml b/res/menu/main.xml
new file mode 100644 (file)
index 0000000..cf1a1e8
--- /dev/null
@@ -0,0 +1,7 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:id="@+id/prefsItem" android:title="@string/menu_preferences_title" android:titleCondensed="Prefs" android:showAsAction="ifRoom" android:icon="@android:drawable/ic_menu_preferences"></item>
+    <item android:id="@+id/localItem" android:showAsAction="ifRoom" android:title="@string/menu_local_commands_title" android:titleCondensed="LOCAL" android:icon="@android:drawable/ic_menu_more"></item>
+    <item android:id="@+id/helpItem" android:title="@string/menu_help_title" android:showAsAction="ifRoom" android:icon="@android:drawable/ic_menu_help"></item>
+    
+
+</menu>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644 (file)
index 0000000..6e75444
--- /dev/null
@@ -0,0 +1,447 @@
+<resources>
+    <string name="app_name">FonBot</string>
+    <string name="send_message_permission_desc">Allows an application to send notifications through FonBot</string>
+    <string name="help">Initial setup:\n
+<ol>
+       <li><a href="http://auth.ieval.ro">Register an account</a>\n</li>
+       <li>Enter your username and password in the <a href="ro.ieval.fonbot.FonBotPreferenceActivity://">preferences</a>\n\n</li>
+</ol>
+
+Sending commands:\n
+<ul>
+    <li>via Jabber: Send commands to fonbot@ieval.ro from any Jabber/GTalk account\n</li>
+    <li>via Yahoo Messenger: Send commands to fonbotym\n</li>
+    <li>via SMS: Set a SMS Password in the <a href="ro.ieval.fonbot.FonBotPreferenceActivity://">preferences</a>, then send SMSes with the SMS Password as the first line. The next lines will be processed as commands\n</li>
+    <li>via email: Send commands to fonbot@ieval.ro\n</li>
+</ul>
+\nFonBot commands consist of a command name and zero or more arguments, separated by spaces. To include spaces in an argument, surround the argument with single or double quotes.\n\n
+Example command: sms 0755555555 \'This is a random text\'. This command would send a text message to 0755555555\n\n
+The help command can be used to get a list of commands and help for them. Examples:\n\n
+<ul>
+    <li>help — shows a list of commands\n</li>
+    <li>help sms — shows help for the ls comand</li>
+</ul>
+</string>
+    <string name="user_tapped_fmt">User tapped \'%s\'</string>
+    <string name="user_navigated_away_from_dialog">User navigated away from dialog</string>
+    <string name="user_canceled_dialog">User canceled dialog</string>
+    <string name="admin_disable_requested">Admin disable requested</string>
+    <string name="admin_enabled">Admin enabled</string>
+    <string name="device_password_changed">Device password changed</string>
+    <string name="device_login_failed_fmt">Device login failed. Wrong password entered %s times</string>
+    <string name="device_login_succeeded">Device login succeeded</string>
+    <string name="logging_in">Logging in…</string>
+    <string name="phone_status_idle">Phone is now idle</string>
+    <string name="phone_status_ringing">"Phone is ringing. Incoming number: "</string>
+    <string name="phone_status_offhook">Phone is offhook</string>
+    <string name="location">Location</string>
+    <string name="latitude">Latitude</string>
+    <string name="longitude">Longitude</string>
+    <string name="accuracy">Accuracy</string>
+    <string name="altitude">Altitude</string>
+    <string name="bearing">Bearing</string>
+    <string name="speed">Speed</string>
+    <string name="at">at</string>
+    <string name="location_provider_disabled">Location provider %s disabled</string>
+    <string name="location_provider_enabled">Location provider %s enabled</string>
+    <string name="location_provider_available">Location provider %s is now available</string>
+    <string name="location_provider_temporary_unavailable">Location provider %s is temporary unavailable</string>
+    <string name="location_provider_out_of_service">Location provider %s is out of service</string>
+    <string name="location_provider_unknown_state">Location provider %s is in an unknown state</string>
+    <string name="no_response_returned_from_server">No response returned from server</string>
+    <string name="connection_error">Connection error</string>
+    <string name="sms_received_fmt">SMS received from %1$s with text %2$s</string>
+    <string name="battery_low">Battery low</string>
+    <string name="battery_okay">Battery okay</string>
+    <string name="plugged_in">plugged in</string>
+    <string name="unplugged">unplugged</string>
+    <string name="headset_with_microphone">Headset (with a microphone)</string>
+    <string name="headset_without_microphone">Headset (without a microphone)</string>
+    <string name="help_activity_label">FonBot Help</string>
+    <string name="foreground_notification_text">FonBot is running</string>
+    <string name="foreground_notification_title">FonBot</string>
+    <string name="phone_numer_type_assistant">Assistant</string>
+    <string name="phone_number_type_callback">Callback</string>
+    <string name="phone_number_type_car">Car</string>
+    <string name="phone_number_type_company_main">Company main</string>
+    <string name="phone_number_type_home_fax">Home fax</string>
+    <string name="phone_number_type_work_fax">Work fax</string>
+    <string name="phone_number_type_home">Home</string>
+    <string name="phone_number_type_isdn">ISDN</string>
+    <string name="phone_number_type_main">Main</string>
+    <string name="phone_number_type_mms">MMS</string>
+    <string name="phone_number_type_mobile">Mobile</string>
+    <string name="phone_number_type_other">Other</string>
+    <string name="phone_number_type_other_fax">Other fax</string>
+    <string name="phone_number_type_pager">Pager</string>
+    <string name="phone_number_type_radio">Radio</string>
+    <string name="phone_number_type_telex">TELEX</string>
+    <string name="phone_number_type_textphone">Textphone</string>
+    <string name="phone_number_type_work">Work</string>
+    <string name="phone_number_type_work_mobile">Work mobile</string>
+    <string name="phone_number_type_work_pager">Work pager</string>
+    <string name="phone_number_type_unknown">Unknown (%d)</string>
+    <string name="ringing">Ringing…</string>
+    <string name="cannot_grab_camera">Cannot grab camera</string>
+    <string name="no_longer_ringing">No longer ringing</string>
+    <string name="no_bluetooth_adapter">No bluetooth adapter</string>
+    <string name="bluetooth_on">Bluetooth: on</string>
+    <string name="bluetooth_off">Bluetooth: off</string>
+    <string name="enabling_bluetooth">Enabling bluetooth…</string>
+    <string name="disabling_bluetooth">Disabling bluetooth…</string>
+    <string name="incoming_call_from">"Incoming call from %s "</string>
+    <string name="missed_call_from">"Missed call from %s "</string>
+    <string name="outgoing_call_to">"Outgoing call to %s "</string>
+    <string name="duration_seconds_starting_at">Duration: %1$d seconds starting at %2$s</string>
+    <string name="no_matching_contacts_found">No matching contacts found</string>
+    <string name="contact_info">Name:  %1$s; Phone: %2$s; Type: %3$s</string>
+    <string name="battery_level">"Battery level: %f%%. "</string>
+    <string name="not_plugged_in">"Not plugged in. "</string>
+    <string name="plugged_in_ac">"Plugged in (AC). "</string>
+    <string name="plugged_in_usb">"Plugged in (USB). "</string>
+    <string name="plugged_in_wireless">"Plugged in (wireless). "</string>
+    <string name="status_charging">"Status: charging. "</string>
+    <string name="status_discharging">"Status: discharging. "</string>
+    <string name="status_full">"Status: full. "</string>
+    <string name="status_not_charging">"Status: not charging. "</string>
+    <string name="status_unknown">Status: unknown. </string>
+    <string name="temperature">"Temperature: %d. "</string>
+    <string name="voltage">Voltage: %d.</string>
+    <string name="dialing">Dialing %s…</string>
+    <string name="menu_preferences_title">Preferences</string>
+    <string name="menu_local_commands_title">Local commands</string>
+    <string name="menu_help_title">Help</string>
+    <string name="showing_dialog">Showing dialog…</string>
+    <string name="last_known_location">"Last known location: "</string>
+    <string name="listening_for_location_updates">Listening for location updates…</string>
+    <string name="device_locked">Device locked</string>
+    <string name="file_not_found">File \'%s\' not found</string>
+    <string name="file_sent">File sent</string>
+    <string name="cannot_connect_to_host_on_port">Cannot connect to host \'%1$s\' on port \'%2$d\'</string>
+    <string name="io_error">I/O Error: %s</string>
+    <string name="no_longer_listening_for_location_updates">No longer listening for location updates</string>
+    <string name="error_setting_preview_display">Error setting preview display</string>
+    <string name="no_help_for_this_command">No help for this command</string>
+    <string name="string_is_not_a_directory">%s is not a directory</string>
+    <string name="files_in_directory">"Files in %s: "</string>
+    <string name="no_ringtone_found">No ringtone found</string>
+    <string name="ringer_mode_normal">Ringer mode: normal</string>
+    <string name="ringer_mode_vibrate">Ringer mode: vibrate</string>
+    <string name="ringer_mode_silent">Ringer mode: silent</string>
+    <string name="unknown_ringer_mode">Unknown ringer mode</string>
+    <string name="file_deleted">File deleted</string>
+    <string name="error_while_deleting_file">Error while deleting file</string>
+    <string name="password_cleared">Password cleared</string>
+    <string name="password_set">Password set</string>
+    <string name="message_was_split_into_parts">Message was split into %d parts</string>
+    <string name="tts_engine_not_available">TTS Engine not available</string>
+    <string name="speaking">Speaking…</string>
+    <string name="toast_shown">Toast shown</string>
+    <string name="vibrating">Vibrating…</string>
+    <string name="url_opened">URL opened</string>
+    <string name="no_activity_found_for_this_url">No activity found for this URL</string>
+    <string name="invalid_url">Invalid URL</string>
+    <string name="wifi_on">Wifi: on</string>
+    <string name="wifi_off">Wifi: off</string>
+    <string name="enabling_wifi">Enabling wifi…</string>
+    <string name="disabling_wifi">Disabling wifi…</string>
+    <string name="polling_server">Polling server…</string>
+    <string name="polling_stopped">Polling stopped</string>
+    <string name="command_disabled">Command disabled%s disabled</string>
+    <string name="command_enabled">Command %s enabled</string>
+    <string name="polling_every_milliseconds">Polling every %d milliseconds</string>
+    <string name="incoming_message">Incoming message from %1$s: \"%2$s\" sent at %3$s</string>
+    <string name="outgoing_message">Outgoing message to %1$s: \"%2$s\" sent at %3$s</string>
+    <string name="error_writing_to_socket">Error writing to socket: %s</string>
+    <string name="cannot_close_socket">Cannot close socket: %s</string>
+    <string name="unknown_host">Unknown host: %s</string>
+    <string name="photo_sent">Photo sent</string>
+    <string name="sending_photo">Sending photo…</string>
+    <string name="invalid_length_allowed_values_are">"Invalid length. Allowed values are: %s"</string>
+    <string name="could_not_parse_argument_allowed_values_are">Could not parse argument. Allowed values are: %s</string>
+    <string name="exception_while_hanging_up_call">Exception while hanging up call %1$s: %2$s</string>
+    <string name="exception_while_answering_call">Exception while answering call %1$s: %2$s</string>
+    <string name="no_such_package">No such package</string>
+    <string name="app_launched">App launched</string>
+    <string name="exception_while_getting_itelephony">Exception while getting ITelephony %1$s: %2$s</string>
+    <string name="data_off">Data: off</string>
+    <string name="data_on">Data: on</string>
+    <string name="disabling_data">Disabling data…</string>
+    <string name="enabling_data">Enabling data…</string>
+    <string name="exception_while_determining_data_state">Exception while determining data state</string>
+    <string name="enabling_gps">Enabling GPS…</string>
+    <string name="disabling_gps">Disabling GPS…</string>
+    <string name="enabling_network_location">Enabling network location…</string>
+    <string name="disabling_network_location">Disabling network location…</string>
+    <string name="gps_on">GPS: on</string>
+    <string name="gps_off">GPS: off</string>
+    <string name="network_location_off">Network location: off</string>
+    <string name="network_location_on">Network location: on</string>
+    <string name="answer_help">
+        Usage: answer\n
+        Answers the phone, if it is ringing. Does nothing otherwise.\n
+        Example: answer
+    </string>
+    <string name="batt_help">
+        Usage: batt\n
+               Shows battery information\n
+               Example: batt
+    </string>
+    <string name="wipe_help">
+               Usage: wipe type \"%s\"\n
+               If &lt;type&gt; is \"data\", wipes the phone, leaving the sdcard intact\n
+               If &lt;type&gt; is \"full\", wipes the phone and the sdcard\n
+               This command requires admin permissions\n
+               WARNING: THIS CANNOT BE UNDONE!\n
+               Example: wipe data
+    </string>
+    <string name="poll_help">
+        Usage: poll [ms]\n
+               With no arguments, polls the server once\n
+               With an argument, starts polling the server every &lt;ms&gt; milliseconds. If &lt;ms&gt; is 0, the polling is stopped\n
+               Example: poll 60000
+    </string>
+    <string name="contacts_help">
+        Usage: contacts substring\n
+               Returns a list of contacts whose name or nickname contains &lt;substring&gt;\n
+               Example: contacts Jack
+    </string>
+    <string name="rm_help">
+        Usage: rm file\n
+               Removes the &lt;file&gt; file\n
+               Example: rm /mnt/sdcard/file
+    </string>
+    <string name="ls_help">
+        Usage: ls directory\n
+               Lists the contents of the &lt;directory&gt; directory\n
+               Example: ls /mnt/sdcard
+    </string>
+    <string name="sms_help">
+        Usage: sms phone_number message\n
+               Sends an sms with text &lt;message&gt; to &lt;phone_number&gt;.\n
+               Example: sms 0755555555 \"Test message\"
+    </string>
+    <string name="setpassword_help">
+        Usage: setpassword [password]\n
+               With no argument, clears the device password.\n
+               With an argument, sets the device password to &lt;password&gt;.\n
+               Example: setpassword verysecurepassword
+    </string>
+    <string name="ring_help">
+        Usage: ring on/off\n
+               Make phone start/stop ringing\n
+               Example: ring on
+    </string>
+    <string name="photo_help">
+        Usage: photo hostname port\n
+               Takes a photo and uploads it to &lt;hostname&gt;:&lt;port&gt;\n
+               Example: photo 1.1.1.1 8888
+    </string>
+    <string name="nolocation_help">
+        Usage: nolocation\n
+               Stops sending location updates.\n
+               Example: nolocation
+    </string>
+    <string name="ncfile_help">
+        Usage: ncfile filename hostname port\n
+               Uploads the &lt;filename&gt; file to &lt;hostname&gt;:&lt;port&gt;\n
+               Example: ncfile /mnt/sdcard/file 1.1.1.1 8888
+    </string>
+    <string name="lock_help">
+        Usage: lock\n
+               Locks the phone. Requires device admin.\n
+               Example: lock
+    </string>
+    <string name="flash_help">
+        Usage: flash on/off\n
+               Turns the flashlight on or off\n
+               Example: flash on
+    </string>
+    <string name="echo_help">
+        Usage: echo message …\n
+               Echoes &lt;message&gt;.\n
+               Example: echo Hello, world!
+    </string>
+    <string name="dial_help">
+        Usage: dial number\n
+               Dials &lt;number&gt;.\n
+               Example: dial 0755555555
+    </string>
+    <string name="wifi_help">
+        Usage: wifi [on/off]\n
+               With no arguments, shows the wifi status\n
+               With an argument, turns the wifi on or off\n
+               Example: wifi on
+    </string>
+    <string name="view_help">
+        Usage: view link\n
+               Opens the link &lt;link&gt; in an appropriate application\n
+               Example: view tel://0755555555
+    </string>
+    <string name="vibrate_help">
+        Usage: vibrate ms\n
+               Makes the phone vibrate for &lt;ms&gt; milliseconds\n
+               Example: vibrate 2500
+    </string>
+    <string name="toast_help">
+        Usage: toast text [length]\n
+               Shows a toast with the &lt;text&gt; message. &lt;length&gt; is the length of the toast (long or short). Default is short.\n
+               Example: toast \"Hello world!\" long
+    </string>
+    <string name="speak_help">
+        Usage: speak text …\n
+               Speaks &lt;text&gt;\n
+               Example: speak Hello, stranger!
+    </string>
+    <string name="smslog_help">
+        Usage: smslog [count]\n
+               With no argument, shows the last 5 SMSes\n
+               With an argument, shows the last &lt;count&gt; SMSes\n
+               Example: smslog 10
+    </string>
+    <string name="play_help">
+        Usage: play\n
+               Starts playing music\n
+               Example: play
+    </string>
+    <string name="pause_help">
+        Usage: pause\n
+               Stops playing music\n
+               Example: pause
+    </string>
+    <string name="next_help">
+        Usage: next\n
+               Skips to the next track\n
+               Example: next
+    </string>
+    <string name="prev_help">
+        Usage: prev\n
+               Skips to the previous track\n
+               Example: prev
+    </string>
+    <string name="bluetooth_help">
+        Usage: bluetooth [on/off]\n
+               With no arguments, prints the bluetooth status.\n
+               With an argument, turns bluetooth on or off.\n
+               Example: bluetooth on
+    </string>
+    <string name="calllog_help">
+        Usage: calllog [count]\n
+               With no arguments, prints the last 5 calls.\n
+               With an argument, prints the last &lt;count&gt; calls\n
+               Example: calllog 10
+    </string>
+    <string name="dialog_help">
+        Usage: dialog message [button] …\n
+               Shows a dialog box with the &lt;message&gt; text and the &lt;button…&gt; buttons. Returns the button pressed\n
+               Example: dialog \"Hello stranger. Thanks for finding my phone, will you return it to me?\" Yes No
+    </string>
+    <string name="enable_help">
+        Usage: enable command\n
+               Enables a command, undoing the effects of a previous DISABLE command\n
+               This command can only be issued from the local command interface\n
+               Command list: %s\n
+               Example: disable toast
+    </string>
+    <string name="disable_help">
+        Usage: disable command\n" +
+               Disables a command. Once disabled, a command cannot be used until enabled with the ENABLE command\n
+               This command can only be issued from the local command interface\n
+               Command list: %s\n
+               Example: disable toast
+    </string>
+    <string name="help_help">
+        Usage: help command\n
+               Prints help for &lt;command&gt;. Available commands are %s\n
+               Example: help batt
+    </string>
+    <string name="setnotification_help">
+        Usage: setnotification notification\n
+               Enables the &lt;notification&gt; notification.\n
+               Messages from this notification will be sent to this address.\n
+               Available notifications: %s\n
+               Example: setnotification sms
+    </string>
+    <string name="delnotification_help">
+        Usage: delnotification notification\n
+               Disables the &lt;notification&gt; notification.\n
+               Available notifications: %s\n
+               Example: delnotification sms
+    </string>
+    <string name="location_help">
+        Usage: location provider [min_time [min_distance]]\n" +
+               Starts sending location updates. Location updates will be sent at most once each min_time (default: 500) milliseconds and only if the phone has moved at least min_distance meters (default: 0) from the last update\n
+               &lt;provider&gt; is the location provider, one of %s\n
+               Example: location network 0 0"
+    </string>
+    <string name="ringer_help">
+        Usage: ringer mode\n
+               Sets the ringer mode. &lt;mode&gt; is one of: %s\n
+               Example: ringer vibrate
+    </string>
+    <string name="data_help">
+        Usage: data [on/off]\n
+        With no arguments, shows the mobile data status\n
+        With an argument, turns the mobile data on or off\n
+        Example: data off 
+    </string>
+    <string name="gps_help">
+        Usage: gps [on/off]\n
+        With no arguments, shows the GPS status\n
+        With an argument, turns the GPS on or off\n
+        Example: gps off
+    </string>
+    <string name="glocation_help">
+        Usage: glocation [on/off]\n
+        With no arguments, shows the Google location service status\n
+        With an argument, turns the Google location service on or off\n
+        NOTE: turning the Google location service on remotely is not possible, since the user must agree to the service disclaimer.\n
+        Example: glocation off
+    </string>
+    <string name="hangup_help">
+        Usage: hangup\n
+        Hangs the phone up.\n
+        Example: hangup
+    </string>
+    <string name="launch_help">
+        Usage: launch package\n
+        Launches the app defined by the &lt;package&gt; package\n
+        Example: launch ro.ieval.fonbot
+    </string>
+    <string name="could_not_parse_ms">Could not parse ms</string>
+    <string name="cannot_parse_provider_allowed_values_are">Cannot parse provider. Allowed values are: %s</string>
+    <string name="cannot_parse_min_time">Cannot parse min_time</string>
+    <string name="cannot_parse_min_distance">Cannot parse min_distance</string>
+    <string name="invalid_ringer_mode_valid_values_are">Invalid ringer mode. Valid values are: %s</string>
+    <string name="cannot_parse_port">Cannot parse port</string>
+    <string name="notification_enabled">Notification enabled</string>
+    <string name="messagetype_should_be_one_of">MessageType should be one of: %s</string>
+    <string name="notification_disabled">Notification disabled</string>
+    <string name="security_exception">Security exception: %s</string>
+    <string name="the_second_argument_to_wipe_must_be">The second argument to wipe must be \"%s\"</string>
+    <string name="wipetype_should_be_one_of">WipeType should be one of: %s</string>
+    <string name="cannot_parse_count">Cannot parse count</string>
+    <string name="no_such_command_command_list">No such command. Command list: %s</string>
+    <string name="cannot_parse_interval">Cannot parse interval</string>
+    <string name="unknown_command">Unknown command: \'%1$s\' (%2$s)</string>
+    <string name="error_while_processing_command">Error while processing command (%1$s: %2$s)</string>
+    <string name="rebooting">Rebooting…</string>
+    <string name="reboot_help">
+        Usage: reboot\n
+        Reboots the phone.\n
+        Example: reboot
+    </string>
+    <string name="shutdown_help">
+        Usage: shutdown\n
+        Shuts the phone down.\n
+        Example: shutdown
+    </string>
+    <string name="location_tracking_is_active">Location tracking is active</string>
+    <string name="the_polling_service_is_running">The polling service is running</string>
+    <string name="cancel">Cancel</string>
+    <string name="command_sent">Command sent</string>
+    <string name="could_not_parse_id">Could not parse id</string>
+    <string name="notification_canceled">Notification canceled</string>
+    <string name="notification_shown">Notification shown</string>
+    <string name="notify_help">Notify help</string>
+</resources>
\ No newline at end of file
diff --git a/res/xml/admin.xml b/res/xml/admin.xml
new file mode 100644 (file)
index 0000000..7b4883d
--- /dev/null
@@ -0,0 +1,8 @@
+<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
+  <uses-policies>
+    <watch-login />
+    <reset-password />
+    <force-lock />
+    <wipe-data />
+  </uses-policies>
+</device-admin>
\ No newline at end of file
diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml
new file mode 100644 (file)
index 0000000..b289fc9
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+    <EditTextPreference android:title="Username" android:key="username" android:dialogTitle="Username" android:dialogMessage="Enter your username"/>
+    <EditTextPreference android:dialogTitle="Password" android:dialogMessage="Enter your password" android:inputType="textPassword" android:title="Password" android:key="password"/>
+    <EditTextPreference android:dialogTitle="SMS Password" android:dialogMessage="Enter the password required to send SMS commands. Leave empty to disable SMS commands" android:title="SMS Password" android:key="smspassword"/>
+    <CheckBoxPreference android:title="Device administration" android:summaryOn="Device administration is enabled." android:key="admin" android:summaryOff="Device administration is disabled. Some commands require device administration."/><CheckBoxPreference android:key="foreground" android:title="Run service in foreground" android:summaryOn="The FonBot service is kept in foreground. " android:summaryOff="The FonBot service is not kept in foreground."/><CheckBoxPreference android:key="poll_on_screen_on" android:summaryOff="The server is not polled when the screen is turned on" android:summaryOn="The server is polled when the screen is turned on" android:title="Poll server on screen on"/>
+    <CheckBoxPreference android:key="system" android:summaryOff="FonBot is not a system app. Changing this requires root and busybox" android:title="System App" android:summaryOn="FonBot is a system app. Changing this requires root and busybox"/>
+    
+</PreferenceScreen>
\ No newline at end of file
diff --git a/src/com/android/internal/telephony/ITelephony.aidl b/src/com/android/internal/telephony/ITelephony.aidl
new file mode 100644 (file)
index 0000000..706271c
--- /dev/null
@@ -0,0 +1,11 @@
+package com.android.internal.telephony;
+
+import android.os.Bundle;
+interface ITelephony {
+       boolean endCall();
+       void dial(String number);
+       void answerRingingCall();
+       boolean enableDataConnectivity();
+       boolean disableDataConnectivity();
+       int getDataState();
+}
\ No newline at end of file
diff --git a/src/org/eclipse/jdt/annotation/NonNull.java b/src/org/eclipse/jdt/annotation/NonNull.java
new file mode 100644 (file)
index 0000000..eaaae3d
--- /dev/null
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (c) 2011, 2012 Stephan Herrmann and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *     IBM Corporation - bug fixes
+ *******************************************************************************/
+package org.eclipse.jdt.annotation;
+
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+/**
+ * Qualifier for a type in a method signature or a local variable declaration:
+ * The entity (return value, parameter, local variable) whose type has this
+ * annotation can never have the value <code>null</code> at runtime.
+ * <p>
+ * This has two consequences:
+ * <ol>
+ * <li>Dereferencing the entity is safe, i.e., no <code>NullPointerException</code> can occur at runtime.</li>
+ * <li>An attempt to bind a <code>null</code> value to the entity is a compile time error.</li>
+ * </ol>
+ * For the second case, diagnostics issued by the compiler should distinguish three situations:
+ * <ol>
+ * <li>Nullness of the value can be statically determined, the entity is definitely bound from either of:
+ *     <ul><li>the value <code>null</code>, or</li>
+ *         <li>an entity with a {@link Nullable @Nullable} type.</li></ul></li>
+ * <li>Nullness cannot definitely be determined, because different code branches yield different results.</li>
+ * <li>Nullness cannot be determined, because other program elements are involved for which
+ *     null annotations are lacking.</li>
+ * </ol>
+ * </p>
+ * @since 1.0
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ METHOD, PARAMETER, LOCAL_VARIABLE })
+public @interface NonNull {
+       // marker annotation with no members
+}
diff --git a/src/org/eclipse/jdt/annotation/NonNullByDefault.java b/src/org/eclipse/jdt/annotation/NonNullByDefault.java
new file mode 100644 (file)
index 0000000..0ef1cac
--- /dev/null
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2011 Stephan Herrmann and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *     IBM Corporation - bug fixes
+ *******************************************************************************/
+package org.eclipse.jdt.annotation;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+/**
+ * This annotation can be applied to a package, type, method or constructor in order to 
+ * define that all contained entities for which a null annotation is otherwise lacking
+ * should be considered as {@link NonNull @NonNull}.
+ * <dl>
+ * <dt>Canceling a default</dt>
+ * <dd>By using a <code>@NonNullByDefault</code> annotation with the argument <code>false</code>,
+ * a default from any enclosing scope can be canceled for the element being annotated.
+ * <dt>Nested defaults</dt>
+ * <dd>If a <code>@NonNullByDefault</code>
+ * annotation is used within the scope of another <code>@NonNullByDefault</code>
+ * annotation or a project-wide default setting, the innermost annotation defines the
+ * default applicable at any given position (depending on the parameter {@link #value()}).</dd>
+ * </dl>
+ * Note that for applying an annotation to a package, a file by the name
+ * <code>package-info.java</code> is used.
+ * 
+ * @since 1.0
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ PACKAGE, TYPE, METHOD, CONSTRUCTOR })
+public @interface NonNullByDefault {
+       /**
+        * When parameterized with <code>false</code>, the annotation specifies that the current element should not apply
+        * any default to un-annotated types.
+        */
+       boolean value() default true;
+}
diff --git a/src/org/eclipse/jdt/annotation/Nullable.java b/src/org/eclipse/jdt/annotation/Nullable.java
new file mode 100644 (file)
index 0000000..e1a6a1b
--- /dev/null
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * Copyright (c) 2011, 2012 Stephan Herrmann and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *     IBM Corporation - bug fixes
+ *******************************************************************************/
+package org.eclipse.jdt.annotation;
+
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+/**
+ * Qualifier for a type in a method signature or a local variable declaration:
+ * The entity (return value, parameter, local variable) whose type has this
+ * annotation is allowed to have the value <code>null</code> at runtime.
+ * <p>
+ * This has two consequences:
+ * <ul>
+ * <li>Binding a <code>null</code> value to the entity is legal.</li>
+ * <li>Dereferencing the entity is unsafe, i.e., a <code>NullPointerException</code> can occur at runtime.</li>
+ * </ul>
+ * </p>
+ * @since 1.0
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ METHOD, PARAMETER, LOCAL_VARIABLE })
+public @interface Nullable {
+       // marker annotation with no members
+}
\ No newline at end of file
diff --git a/src/org/eclipse/jdt/annotation/package-info.java b/src/org/eclipse/jdt/annotation/package-info.java
new file mode 100644 (file)
index 0000000..78fe2f6
--- /dev/null
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2011 Stephan Herrmann and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *     IBM Corporation - bug fixes
+ *******************************************************************************/
+
+/**
+ * This package contains annotations that can trigger special behavior
+ * when annotated types are compiled by the Eclipse Compiler for Java.
+ * <p>
+ * Currently, the package contains annotations that specify nullness contracts
+ * for annotated elements.
+ * </p>
+ */
+package org.eclipse.jdt.annotation;
+
diff --git a/src/ro/ieval/fonbot/Address.java b/src/ro/ieval/fonbot/Address.java
new file mode 100644 (file)
index 0000000..59fc0d7
--- /dev/null
@@ -0,0 +1,98 @@
+package ro.ieval.fonbot;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * An address to which messages can be sent using {@link Utils#sendMessage(android.content.Context, Address, int)}
+ * 
+ * Addresses are defined by a protocol and a data part. The data is a protocol-dependent opaque String.
+ * 
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+final class Address {
+       /**
+        * The protocol of an Address.
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       public static enum Protocol{
+               /**
+                * The protocol used for sending messages via short text messages. The data for a SMS address is the phone number.
+                */
+               SMS,
+               /**
+                * The protocol used for sending messages via anything handled by the iEval server. The data is an opaque String supplied by the iEval server.
+                */
+               HTTP,
+               /**
+                * The protocol used for sending messages to {@link FonBotLocalActivity}
+                */
+               LOCAL,
+               /**
+                * The protocol used for suppressing messages.
+                */
+               NULL
+       }
+
+       /**
+        * An address for suppressing messages
+        */
+       public static final Address BLACKHOLE=new Address(Utils.toNonNull(Protocol.NULL), null); 
+       /**
+        * The protocol part of the Address.
+        */
+       public final Protocol protocol;
+       /**
+        * The data part of the Address. Can be null
+        */
+       public final String data;
+
+       /**
+        * Construct an Address from its parts
+        * 
+        * @param protocol the protocol part of the Address
+        * @param data the data part of the Address
+        */
+       public Address(final Protocol protocol, final @Nullable String data){
+               this.protocol=protocol;
+               this.data=data;
+       }
+
+       /**
+        * Construct an Address from its string representation (the protocol and data in this order separated by a single space character). Does the reverse of {@link #toString()}
+        * 
+        * @param address the Address string representation
+        * @throws IllegalArgumentException if the protocol part is not a member of the {@link Protocol} enum
+        */
+       public Address(final String address) throws IllegalArgumentException{
+               final String[] parts=address.split(" ", 2);
+               this.protocol=Protocol.valueOf(parts[0]);
+               this.data=parts[1];
+       }
+
+       /**
+        * Returns the string representation of an Address, which is the protocol and data in this order separated by a single space character
+        */
+       @Override
+       public String toString(){
+               return protocol+" "+data;
+       }
+}
diff --git a/src/ro/ieval/fonbot/BackupAgent.java b/src/ro/ieval/fonbot/BackupAgent.java
new file mode 100644 (file)
index 0000000..582375d
--- /dev/null
@@ -0,0 +1,36 @@
+package ro.ieval.fonbot;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * Backup agent. Backups the preferences file
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class BackupAgent extends BackupAgentHelper {
+       @Override
+       public void onCreate() {
+               addHelper("prefs", new SharedPreferencesBackupHelper(this,
+                               getClass().getPackage().getName()+"_preferences"));
+       }
+}
diff --git a/src/ro/ieval/fonbot/DialogActivity.java b/src/ro/ieval/fonbot/DialogActivity.java
new file mode 100644 (file)
index 0000000..89ae59c
--- /dev/null
@@ -0,0 +1,109 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * A dialog created by the {@link Utils.Command#DIALOG DIALOG} command. Includes a message and a list of buttons, all user-defined
+ * 
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class DialogActivity extends Activity {
+       /** Extra: string array of dialog buttons */
+       public static final String EXTRA_BUTTONS = "buttons";
+       /** Extra: dialog message */
+       public static final String EXTRA_MESSAGE = "message";
+       /** Extra: reply Address */
+       public static final String EXTRA_REPLYTO = "replyto";
+
+       /** true if the dialog is finishing because the user tapped a button, false otherwise */
+       private boolean gotAnswer=false;
+       /** Reply address */
+       private Address address;
+
+       @Override
+       protected void onCreate(@Nullable final Bundle icicle) {
+               super.onCreate(icicle);
+               address=new Address(toNonNull(getIntent().getStringExtra(EXTRA_REPLYTO)));
+               final LinearLayout layout=new LinearLayout(this);
+               layout.setOrientation(LinearLayout.VERTICAL);
+               if(getIntent().hasExtra(EXTRA_MESSAGE)){
+                       final String text=getIntent().getStringExtra(EXTRA_MESSAGE);
+                       final TextView tv=new TextView(this);
+                       tv.setText(text);
+                       layout.addView(tv);
+               }
+               final String[] buttonLabels=getIntent().getStringArrayExtra(EXTRA_BUTTONS);
+
+               final OnClickListener buttonListener=new OnClickListener() {
+                       @Override
+                       public void onClick(@Nullable final View v) {
+                               if(v==null)
+                                       return;
+
+                               Utils.sendMessage(DialogActivity.this, toNonNull(address), 
+                                               toNonNull(getString(user_tapped_fmt, ((Button)v).getText())));
+                               gotAnswer=true;
+                               finish();
+                       }
+               };
+               for(final String label : buttonLabels){
+                       final Button b=new Button(this);//NOPMD buttons must be different
+                       b.setText(label);
+                       b.setOnClickListener(buttonListener);
+                       layout.addView(b);
+               }
+               setContentView(layout);
+       }
+       
+       @Override
+       protected void onStop() {
+               super.onStop();
+               if(!gotAnswer){
+                       Utils.sendMessage(this, toNonNull(address),
+                                       toNonNull(getString(user_navigated_away_from_dialog)));
+                       gotAnswer=true;
+               }
+               finish();
+       }
+
+       @Override
+       public void onBackPressed() {
+               if(!gotAnswer){
+                       Utils.sendMessage(this, toNonNull(address),
+                                       toNonNull(getString(user_canceled_dialog)));
+                       gotAnswer=true;
+               }
+               finish();
+       }
+}
diff --git a/src/ro/ieval/fonbot/DynamicEventReceiver.java b/src/ro/ieval/fonbot/DynamicEventReceiver.java
new file mode 100644 (file)
index 0000000..926280b
--- /dev/null
@@ -0,0 +1,78 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.Utils.MessageType;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.preference.PreferenceManager;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * A BroadcastReceiver that receives "dynamic" events like {@link Intent#ACTION_SCREEN_ON ACTION_SCREEN_ON}, {@link Intent#ACTION_BATTERY_CHANGED ACTION_BATTERY_CHANGED} and {@link Intent#ACTION_HEADSET_PLUG} 
+ * 
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class DynamicEventReceiver extends BroadcastReceiver {
+
+       @Override
+       public void onReceive(final @Nullable Context context, final @Nullable Intent intent) {
+               if(context==null || intent==null || isInitialStickyBroadcast())
+                       return;
+
+               final String action=intent.getAction();
+
+               if(action.equals(Intent.ACTION_SCREEN_ON)){
+                       if(PreferenceManager.getDefaultSharedPreferences(context).getBoolean("poll_on_screen_on", false))
+                               new PollServerAsyncTask().execute();
+               } else if(action.equals(Intent.ACTION_BATTERY_CHANGED))
+                       Heavy.describeBatteryLevel(context, null, toNonNull(MessageType.BATTERY_CHANGED));
+               else if(action.equals(Intent.ACTION_HEADSET_PLUG)){
+                       final int state=intent.getIntExtra("state", 0);
+                       final String name=intent.getStringExtra("name");
+                       final int microphone=intent.getIntExtra("microphone", 0);
+
+                       final StringBuilder builder=new StringBuilder(100);
+                       if(microphone==1)
+                               builder.append(toNonNull(context.getString(headset_with_microphone)));
+                       else
+                               builder.append(toNonNull(context.getString(headset_without_microphone)));
+                       builder.append(' ');
+                       builder.append(name);
+                       builder.append(' ');
+                       if(state==1)
+                               builder.append(toNonNull(context.getString(plugged_in)));
+                       else
+                               builder.append(toNonNull(context.getString(unplugged)));
+                       builder.append(". ");
+
+                       Utils.sendMessage(context,
+                                       toNonNull(MessageType.HEADSET),
+                                       toNonNull(builder.toString()));
+               }
+       }
+
+}
diff --git a/src/ro/ieval/fonbot/FonBotAdminReceiver.java b/src/ro/ieval/fonbot/FonBotAdminReceiver.java
new file mode 100644 (file)
index 0000000..ca1102c
--- /dev/null
@@ -0,0 +1,91 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.Utils.MessageType;
+import android.app.admin.DeviceAdminReceiver;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * The {@link DeviceAdminReceiver} used by FonBot. Sends {@link Utils.MessageType#ADMIN ADMIN} and {@link Utils.MessageType#WATCH_LOGIN WATCH_LOGIN} notifications. 
+ * 
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class FonBotAdminReceiver extends DeviceAdminReceiver {
+       @Override
+       public void onDisabled(@Nullable final Context context, @Nullable final Intent intent) {
+               if(context==null)
+                       return;
+               Utils.sendMessage(context, toNonNull(MessageType.ADMIN), "Admin disabled");
+       }
+
+       @Override
+       public @Nullable CharSequence onDisableRequested(@Nullable final Context context, @Nullable final Intent intent) {
+               if(context==null){
+                       Log.wtf(getClass().getName(), "context is null in onDisableRequested");
+                       throw new AssertionError("Log.wtf did not terminate the process");
+               }
+               Utils.sendMessage(context, toNonNull(MessageType.ADMIN),
+                               toNonNull(context.getString(admin_disable_requested)));
+               return null;//TODO: write this
+       }
+
+       @Override
+       public void onEnabled(@Nullable final Context context, @Nullable final Intent intent) {
+               if(context==null)
+                       return;
+               Utils.sendMessage(context, toNonNull(MessageType.ADMIN),
+                               toNonNull(context.getString(admin_enabled)));
+       }
+
+       @Override
+       public void onPasswordChanged(@Nullable final Context context, @Nullable final Intent intent) {
+               if(context==null)
+                       return;
+               Utils.sendMessage(context, toNonNull(MessageType.ADMIN),
+                               toNonNull(context.getString(device_password_changed)));
+       }
+
+       @SuppressWarnings("boxing")
+       @Override
+       public void onPasswordFailed(@Nullable final Context context, @Nullable final Intent intent) {
+               if(context==null)
+                       return;
+               final DevicePolicyManager dpm=(DevicePolicyManager) FonBotApplication.instance.getSystemService(Context.DEVICE_POLICY_SERVICE);
+               Utils.sendMessage(context, toNonNull(MessageType.WATCH_LOGIN),
+                               toNonNull(context.getString(device_login_failed_fmt, dpm.getCurrentFailedPasswordAttempts())));
+       }
+
+       @Override
+       public void onPasswordSucceeded(@Nullable final Context context, @Nullable final Intent intent) {
+               if(context==null)
+                       return;
+               Utils.sendMessage(context, toNonNull(MessageType.WATCH_LOGIN), toNonNull(context.getString(device_login_succeeded)));
+       }
+}
diff --git a/src/ro/ieval/fonbot/FonBotApplication.java b/src/ro/ieval/fonbot/FonBotApplication.java
new file mode 100644 (file)
index 0000000..46a0328
--- /dev/null
@@ -0,0 +1,66 @@
+package ro.ieval.fonbot;
+
+import android.app.Application;
+import android.content.Intent;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+
+import com.google.android.gcm.GCMRegistrar;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * The {@link Application} class of FonBot. Registers the device with GCM, starts the TTS service and keeps global state.
+ * 
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class FonBotApplication extends Application {
+       /**
+        * iEval GCM sender id
+        */
+       public static final String GCM_SENDER_ID = "379674287523";
+       /**
+        * The one instance of FonBotApplication
+        */
+       public static FonBotApplication instance=null;
+       /**
+        * The GCM registration ID
+        */
+       public String regID=null;
+
+       @Override
+       public void onCreate() {
+               super.onCreate();
+               //GCMRegistrar.checkDevice(this);
+               regID=GCMRegistrar.getRegistrationId(this);
+               if("".equals(regID))
+                       GCMRegistrar.register(this, GCM_SENDER_ID);
+
+               final TelephonyManager tman=(TelephonyManager) getSystemService(TELEPHONY_SERVICE);
+               tman.listen(new FonBotPhoneStateListener(this), PhoneStateListener.LISTEN_CALL_STATE);
+               instance=this;
+
+               new PollServerAsyncTask().execute();
+
+               startService(new Intent(this, FonBotMainService.class));
+
+//             Thread.setDefaultUncaughtExceptionHandler(new RemoteCrashdumpHandler(this));
+       }
+}
diff --git a/src/ro/ieval/fonbot/FonBotHelpActivity.java b/src/ro/ieval/fonbot/FonBotHelpActivity.java
new file mode 100644 (file)
index 0000000..af5f2b6
--- /dev/null
@@ -0,0 +1,46 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.widget.TextView;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * An activity that shows FonBot help.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class FonBotHelpActivity extends Activity {
+       @Override
+       protected void onCreate(final @Nullable Bundle savedInstanceState) {
+               super.onCreate(savedInstanceState);
+               final TextView textView=new TextView(this);
+               textView.setText(help);
+               textView.setMovementMethod(LinkMovementMethod.getInstance());
+               setTitle(help_activity_label);
+               setContentView(textView);
+       }
+}
diff --git a/src/ro/ieval/fonbot/FonBotLocalActivity.java b/src/ro/ieval/fonbot/FonBotLocalActivity.java
new file mode 100644 (file)
index 0000000..ea65940
--- /dev/null
@@ -0,0 +1,124 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+import static ro.ieval.fonbot.Address.Protocol.LOCAL;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * Activity that lets users send commands from inside the app.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class FonBotLocalActivity extends Activity {
+       /**
+        * BroadcastReceiver that receives command responses. 
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       private final class ReplyReceiver extends BroadcastReceiver {
+
+               @Override
+               public void onReceive(final @Nullable Context context, final @Nullable Intent intent) {
+                       if (context == null || intent == null) return;
+                       consoleTextView.setText(consoleTextView.getText() +
+                                       intent.getStringExtra(EXTRA_RESPONSE) + "\n");
+               }
+       }
+
+       /**
+        * The extra name for the command response.
+        *
+        * @see #RESPONSE_RECEIVED_ACTION
+        */
+       public static final String EXTRA_RESPONSE = "response";
+       /**
+        * The action for sending back responses to commands.
+        */
+       public static final String RESPONSE_RECEIVED_ACTION="ro.ieval.fonbot.LOCAL_RESPONSE";
+       /**
+        * Intent filter that catches {@link #RESPONSE_RECEIVED_ACTION}
+        */
+       private static final IntentFilter INTENT_FILTER = new IntentFilter(RESPONSE_RECEIVED_ACTION);
+       /**
+        * Reply address for local commands
+        */
+       private static final Address LOCAL_ADDRESS=new Address(toNonNull(LOCAL), null);
+       /**
+        * The one instance of {@link ReplyReceiver} 
+        */
+       private final ReplyReceiver replyReceiver = new ReplyReceiver();
+       /**
+        * The TextView that contains command responses
+        */
+       private TextView consoleTextView;
+
+       @Override
+       protected void onCreate(final @Nullable Bundle savedInstanceState) {
+               super.onCreate(savedInstanceState);
+               setContentView(R.layout.local);
+
+               consoleTextView = (TextView) findViewById(R.id.consoleTextView);
+               final EditText commandEditText = (EditText) findViewById(R.id.argEditText);
+               commandEditText.setOnEditorActionListener(new OnEditorActionListener() {
+                       @Override
+                       public boolean onEditorAction(final @Nullable TextView v, final int actionId, final @Nullable KeyEvent event) {
+                               if(actionId==EditorInfo.IME_ACTION_SEND||
+                                               actionId==EditorInfo.IME_NULL){
+                                       final String[] commandWithArgs = Utils.shellwords(toNonNull(commandEditText.getText().toString()));
+                                       final String command=commandWithArgs[0];
+                                       final String[] args=new String[commandWithArgs.length-1];
+                                       System.arraycopy(commandWithArgs, 1, args, 0, args.length);
+                                       commandEditText.setText("");
+                                       Utils.processCommand(FonBotLocalActivity.this, toNonNull(command), args,
+                                                       toNonNull(LOCAL_ADDRESS));
+                                       return true;
+                               }
+                               return false;
+                       }
+               });
+       }
+
+       @Override
+       protected void onResume() {
+               super.onResume();
+               this.registerReceiver(replyReceiver, INTENT_FILTER);
+       }
+
+       @Override
+       protected void onPause() {
+               super.onPause();
+               this.unregisterReceiver(replyReceiver);
+       }
+}
\ No newline at end of file
diff --git a/src/ro/ieval/fonbot/FonBotMainActivity.java b/src/ro/ieval/fonbot/FonBotMainActivity.java
new file mode 100644 (file)
index 0000000..92a81af
--- /dev/null
@@ -0,0 +1,258 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import java.util.Arrays;
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.FonBotMainService.Binder;
+import ro.ieval.fonbot.Utils.OngoingEvent;
+
+import android.app.ListActivity;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.v4.content.LocalBroadcastManager;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * Main Activity. Shows the login status and a list of ongoing events.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class FonBotMainActivity extends ListActivity {
+       /**
+        * Adapter for the ongoing event list. Shows a list of ongoing events with cancel buttons.
+        *
+        * @author Marius Gavrilescu 
+        */
+       private static final class ArrayAdapter extends android.widget.ArrayAdapter<OngoingEvent>{
+               /**
+                * Constructs an ArrayAdapter with a given list of events
+                *
+                * @param context Context instance
+                * @param objects the list of events
+                */
+               public ArrayAdapter(final Context context, final OngoingEvent[] objects) {
+                       super(context, 0, 0, objects);
+               }
+
+               @Override
+               public View getView(final int position, final @Nullable View convertView, final @Nullable ViewGroup parent) {
+                       final LayoutInflater inflater=(LayoutInflater) getContext().getSystemService(LAYOUT_INFLATER_SERVICE);
+                       final View listItem;
+
+                       if(convertView instanceof LinearLayout){
+                               listItem=convertView;
+                       } else
+                               listItem=inflater.inflate(R.layout.ongoing_list_item, parent, false);
+
+                       final Button button=(Button) listItem.findViewById(R.id.ongoingItemButton);
+                       final TextView textView=(TextView) listItem.findViewById(R.id.ongoingItemTextView);
+                       final OngoingEvent event=getItem(position);
+
+                       textView.setText(event.resource);
+                       button.setOnClickListener(new OnClickListener() {
+                               @Override
+                               public void onClick(final @Nullable View v) {
+                                       Heavy.cancelOngoing(toNonNull(getContext()), event);
+                               }
+                       });
+
+                       return listItem;
+               }
+       }
+
+       /**
+        * ServiceConnection for getting the ongoing event list from {@link FonBotMainService}
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       private final class ServiceConnection implements android.content.ServiceConnection{
+               /** Binder got from onServiceConnected */
+               private Binder binder;
+
+               @Override
+               public void onServiceDisconnected(final @Nullable ComponentName name) {
+                       // do nothing
+               }
+
+               @Override
+               public void onServiceConnected(final @Nullable ComponentName name, final @Nullable IBinder service) {
+                       if(service==null)
+                               return;
+                       binder=(Binder) service;
+                       refreshAdapter();
+               }
+
+               /**
+                * Updates the list of ongoing events from the service.
+                */
+               public void refreshAdapter(){
+                       if(binder==null)
+                               return;
+                       setListAdapter(new ArrayAdapter(FonBotMainActivity.this,
+                                       toNonNull(binder.getService().getOngoingEvents().toArray(new OngoingEvent[0]))));
+               }
+       }
+
+       /**
+        * BroadcastReceiver that refreshes the ongoing event list.
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       private final class OngoingUpdateReceiver extends BroadcastReceiver{
+               @Override
+               public void onReceive(final @Nullable Context context, final @Nullable Intent intent) {
+                       connection.refreshAdapter();
+               }
+       }
+
+       /**
+        * BroadcastReceiver that updates {@link #resultTextView}
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       private final class LoginReceiver extends BroadcastReceiver {
+               @Override
+               public void onReceive(@Nullable final Context context, @Nullable final Intent intent) {
+                       if(intent==null)
+                               return;
+                       resultTextView.setText(intent.getStringExtra(SendHttpMessageAsyncTask.EXTRA_RESPONSE_MESSAGE));
+                       final int responseCode=intent.getIntExtra(SendHttpMessageAsyncTask.EXTRA_RESPONSE_CODE, 0);
+                       if(responseCode>=200&&responseCode<300)
+                               resultTextView.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.tick, 0, 0);
+                       else
+                               resultTextView.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.cross, 0, 0);
+               }
+       }
+
+       /**
+        * The one instance of {@link OngoingUpdateReceiver}
+        */
+       private final BroadcastReceiver ongoingUpdateReceiver=new OngoingUpdateReceiver();
+       /**
+        * IntentFilter for {@link #ongoingUpdateReceiver}
+        */
+       private static final IntentFilter ONGOING_UPDATE_FILTER=new IntentFilter(FonBotMainService.ACTION_ONGOING_UPDATE);
+       /**
+        * The one instance of {@link LoginReceiver}
+        */
+       private final BroadcastReceiver loginReceiver=new LoginReceiver();
+       /**
+        * The broadcast sent by {@link SendHttpMessageAsyncTask} when logging in. 
+        */
+       private static final String LOGIN_BROADCAST="ro.ieval.fonbot.LOGIN_RESULT";
+       /**
+        * IntentFilter for {@link #loginReceiver}
+        */
+       private static final IntentFilter LOGIN_FILTER=new IntentFilter(LOGIN_BROADCAST);
+       /**
+        * The one instance of {@link ServiceConnection}
+        */
+       private final ServiceConnection connection=new ServiceConnection();
+
+       /**
+        * TextView that tells the user whether logging in failed or succeded.
+        */
+       private TextView resultTextView;
+
+       @Override
+       protected void onCreate(@Nullable final Bundle icicle) {
+               super.onCreate(icicle);
+
+               setContentView(R.layout.main);
+
+               resultTextView=(TextView) findViewById(R.id.resultTextView);
+       }
+
+       @Override
+       protected void onStart() {
+               super.onStart();
+               final SharedPreferences sp=PreferenceManager.getDefaultSharedPreferences(this);
+               resultTextView.setText(logging_in);
+               new SendHttpMessageAsyncTask(toNonNull(Arrays.asList(
+                               new Header("X-Action","LOGIN")
+                               )),LOGIN_BROADCAST,this).execute(
+                                               sp.getString("username", null),
+                                               sp.getString("password", null)
+                                               );
+               connection.refreshAdapter();
+       }
+
+       @Override
+       protected void onResume() {
+               super.onResume();
+               registerReceiver(loginReceiver, LOGIN_FILTER);
+               LocalBroadcastManager.getInstance(this).registerReceiver(ongoingUpdateReceiver, ONGOING_UPDATE_FILTER);
+               bindService(new Intent(this, FonBotMainService.class), connection, 0);
+       }
+
+       @Override
+       protected void onPause() {
+               super.onPause();
+               unregisterReceiver(loginReceiver);
+               LocalBroadcastManager.getInstance(this).unregisterReceiver(ongoingUpdateReceiver);
+               unbindService(connection);
+       }
+
+       @Override
+       public boolean onCreateOptionsMenu(@Nullable final Menu menu) {
+               getMenuInflater().inflate(R.menu.main, menu);
+               return true;
+       }
+
+       @Override
+       public boolean onOptionsItemSelected(@Nullable final MenuItem item) {
+               if(item==null)
+                       return super.onOptionsItemSelected(item);
+               switch(item.getItemId()){
+               case R.id.prefsItem:
+                       startActivity(new Intent(this, FonBotPreferenceActivity.class));
+                       return true;
+               case R.id.localItem:
+                       startActivity(new Intent(this, FonBotLocalActivity.class));
+                       return true;
+               case R.id.helpItem:
+                       startActivity(new Intent(this, FonBotHelpActivity.class));
+                       return true;
+               default:
+                       return super.onOptionsItemSelected(item);
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/ro/ieval/fonbot/FonBotMainService.java b/src/ro/ieval/fonbot/FonBotMainService.java
new file mode 100644 (file)
index 0000000..a751d23
--- /dev/null
@@ -0,0 +1,179 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.Utils.OngoingEvent;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.content.LocalBroadcastManager;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * Main service.
+ *
+ * @author Marius Gavrilescu
+ */
+public final class FonBotMainService extends Service {
+       /**
+        * Binder for the service. It lets clients get a reference to the service.
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       public final class Binder extends android.os.Binder{
+               /**
+                * Get a reference to the {@link FonBotMainService}
+                * @return a reference to the {@link FonBotMainService} 
+                */
+               public FonBotMainService getService(){
+                       return FonBotMainService.this;
+               }
+       }
+
+       /**
+        * Broadcast action: add an ongoing event
+        */
+       public static final String ACTION_PUT_ONGOING="ro.ieval.fonbot.FonBotMainService.ACTION_PUT_ONGOING";
+       /**
+        * Broadcast action: remove an ongoing event
+        */
+       public static final String ACTION_DELETE_ONGOING="ro.ieval.fonbot.FonBotMainService.ACTION_DELETE_ONGOING";
+       /**
+        * Extra: ongoing event id
+        *
+        * @see OngoingEvent
+        * @see #ACTION_DELETE_ONGOING
+        * @see #ACTION_PUT_ONGOING
+        */
+       public static final String EXTRA_ONGOING_ID="ro.ieval.fonbot.FonBotMainService.EXTRA_ONGOING_ID";
+       /**
+        * Broadcast sent when the ongoing event list is updated.
+        */
+       public static final String ACTION_ONGOING_UPDATE="ro.ieval.fonbot.FonBotMainService.ACTION_ONGOING_UPDATE";
+       /**
+        * IntentFilter for events caught by the {@link DynamicEventReceiver}
+        */
+       private static final IntentFilter DYNAMIC_BROADCAST_FILTER=new IntentFilter();
+
+       static{
+               DYNAMIC_BROADCAST_FILTER.addAction(Intent.ACTION_SCREEN_ON);
+               DYNAMIC_BROADCAST_FILTER.addAction(Intent.ACTION_BATTERY_CHANGED);
+               DYNAMIC_BROADCAST_FILTER.addAction(Intent.ACTION_HEADSET_PLUG);
+       }
+
+       /**
+        * The one instance of {@link DynamicEventReceiver}
+        */
+       private final  DynamicEventReceiver receiver=new DynamicEventReceiver();
+       /**
+        * Set of ongoing events.
+        */
+       private final Set<OngoingEvent> ongoing=new HashSet<OngoingEvent>(10);
+
+       /**
+        * Get the set of ongoing events.
+        *
+        * @return a set of ongoing events
+        */
+       public Set<OngoingEvent> getOngoingEvents(){
+               return toNonNull(Collections.unmodifiableSet(ongoing));
+       }
+
+       @Override
+       public @Nullable IBinder onBind(final @Nullable Intent intent) {
+               return new Binder();
+       }
+
+       @Override
+       public void onCreate() {
+               super.onCreate();
+               registerReceiver(receiver, DYNAMIC_BROADCAST_FILTER);
+       }
+
+       @Override
+       public void onDestroy() {
+               super.onDestroy();
+               unregisterReceiver(receiver);
+       }
+
+       @Override
+       public int onStartCommand(final @Nullable Intent intent, final int flags, final int startId) {
+               if(intent!=null && intent.getAction()==ACTION_PUT_ONGOING && intent.hasExtra(EXTRA_ONGOING_ID)){
+                       ongoing.add(OngoingEvent.values()[intent.getIntExtra(EXTRA_ONGOING_ID, 0)]);
+                       LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(ACTION_ONGOING_UPDATE));
+               }
+               if(intent!=null && intent.getAction()==ACTION_DELETE_ONGOING && intent.hasExtra(EXTRA_ONGOING_ID)){
+                       ongoing.remove(OngoingEvent.values()[intent.getIntExtra(EXTRA_ONGOING_ID, 0)]);
+                       LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(ACTION_ONGOING_UPDATE));
+               }
+
+               final boolean runForeground=PreferenceManager.getDefaultSharedPreferences(this).getBoolean("foreground", false);
+               if(!runForeground)
+                       stopForeground(true);
+
+               if(runForeground||!ongoing.isEmpty()){
+                       final Intent mainIntent=new Intent(this, FonBotMainActivity.class);
+                       mainIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                       final NotificationCompat.Builder builder=new NotificationCompat.Builder(this).
+                                       setContentText(getString(foreground_notification_text)).
+                                       setContentTitle(getString(foreground_notification_title)).
+                                       setSmallIcon(android.R.drawable.stat_notify_sync_noanim).
+                                       setPriority(NotificationCompat.PRIORITY_MIN).
+                                       setContentIntent(PendingIntent.getActivity(this, 0, mainIntent, 0)).
+                                       setOngoing(true);
+
+                       final Notification notification;
+                       if(ongoing.isEmpty())
+                               notification=builder.build();
+                       else {
+                               final NotificationCompat.InboxStyle inboxBuilder=new NotificationCompat.InboxStyle(builder);
+
+                               for(final OngoingEvent event : ongoing)
+                                       inboxBuilder.addLine(getString(event.resource));
+
+                               notification=inboxBuilder.build();
+                       }
+
+                       if(runForeground)
+                               startForeground(1337, builder.build());
+                       else{
+                               final NotificationManager man=(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+                               man.notify(1337, notification);
+                       }
+               }
+
+               return START_STICKY;
+       }
+}
diff --git a/src/ro/ieval/fonbot/FonBotPhoneStateListener.java b/src/ro/ieval/fonbot/FonBotPhoneStateListener.java
new file mode 100644 (file)
index 0000000..88bde87
--- /dev/null
@@ -0,0 +1,84 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.phone_status_idle;
+import static ro.ieval.fonbot.R.string.phone_status_offhook;
+import static ro.ieval.fonbot.R.string.phone_status_ringing;
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.Utils.MessageType;
+import android.content.Context;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * PhoneStateListener that sends {@link MessageType#PHONE_STATE} notifications.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+final class FonBotPhoneStateListener extends PhoneStateListener {
+       /**
+        * Context instance used in this class.
+        */
+       private final Context context;
+
+       /**
+        * Construct a FonBotPhoneStateListener with a given context.
+        *
+        * @param context Context instance
+        */
+       public FonBotPhoneStateListener(final Context context) {
+               super();
+               this.context=context;
+       }
+
+       @Override
+       public void onCallStateChanged(final int state, @Nullable final String incomingNumber) {
+               @SuppressWarnings("hiding")
+               final Context context=this.context;
+               if(context==null)
+                       return;
+
+               switch(state){
+               case TelephonyManager.CALL_STATE_IDLE:
+                       Utils.sendMessage(context, toNonNull(MessageType.PHONE_STATE),
+                                       toNonNull(context.getString(phone_status_idle)));
+                       break;
+               case TelephonyManager.CALL_STATE_RINGING:
+                       if(incomingNumber==null)
+                               return;
+                       final String name=Utils.callerId(context, incomingNumber);
+                       if(name==null)
+                               Utils.sendMessage(context, toNonNull(MessageType.PHONE_STATE),
+                                               context.getString(phone_status_ringing)+incomingNumber);
+                       else
+                               Utils.sendMessage(context, toNonNull(MessageType.PHONE_STATE),
+                                               context.getString(phone_status_ringing)+incomingNumber+" ("+name+")");
+                       break;
+               case TelephonyManager.CALL_STATE_OFFHOOK:
+                       Utils.sendMessage(context, toNonNull(MessageType.PHONE_STATE),
+                                       toNonNull(context.getString(phone_status_offhook)));
+                       break;
+               }
+       }
+}
diff --git a/src/ro/ieval/fonbot/FonBotPreferenceActivity.java b/src/ro/ieval/fonbot/FonBotPreferenceActivity.java
new file mode 100644 (file)
index 0000000..848a436
--- /dev/null
@@ -0,0 +1,149 @@
+package ro.ieval.fonbot;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+import android.widget.Toast;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * Preference activity. Lets the user change app preferences.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class FonBotPreferenceActivity extends PreferenceActivity {
+       /**
+        * Preference for toggling device admin permissions.
+        */
+       private CheckBoxPreference adminPreference;
+
+       @SuppressWarnings("deprecation")
+       @Override
+       protected void onCreate(@Nullable final Bundle icicle) {
+               super.onCreate(icicle);
+               setTitle("FonBot Preferences");
+               addPreferencesFromResource(R.xml.prefs);
+
+               adminPreference=(CheckBoxPreference) findPreference("admin");
+               final DevicePolicyManager dpm=(DevicePolicyManager) getSystemService(DEVICE_POLICY_SERVICE);
+               final ComponentName adminReceiver=new ComponentName(this,FonBotAdminReceiver.class);
+               adminPreference.setChecked(dpm.isAdminActive(adminReceiver));
+
+               adminPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+                       
+                       @Override
+                       public boolean onPreferenceChange(final @Nullable Preference preference, final @Nullable Object newValue) {
+                               if(newValue==null){
+                                       Log.wtf(FonBotPreferenceActivity.class.getName(), "newValue in OnPreferenceChange is null");
+                                       throw new AssertionError("Log.wtf did not terminate the process");
+                               }
+
+                               final boolean isChecked=((Boolean) newValue).booleanValue();
+                               if(isChecked){
+                                       if(dpm.isAdminActive(adminReceiver))
+                                               return true;
+
+                                       final Intent intent=new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
+                                       intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, adminReceiver);
+                                       startActivityForResult(intent, 0);
+                                       return false;
+                               }
+                               dpm.removeActiveAdmin(adminReceiver);
+                               return true;
+                       }
+               });
+
+               final CheckBoxPreference foregroundPreference=(CheckBoxPreference) findPreference("foreground");
+               foregroundPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+                       @Override
+                       public boolean onPreferenceChange(final @Nullable Preference preference, final @Nullable Object newValue) {
+                               startService(new Intent(FonBotPreferenceActivity.this, FonBotMainService.class));
+                               return true;
+                       }
+               });
+
+               final CheckBoxPreference systemPreference=(CheckBoxPreference) findPreference("system");
+               final ApplicationInfo info=getApplicationInfo();
+               final boolean isSystem=(info.flags&ApplicationInfo.FLAG_SYSTEM)!=0;
+               systemPreference.setChecked(isSystem);
+               systemPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+                       @Override
+                       public boolean onPreferenceChange(final @Nullable Preference preference, final @Nullable Object newValue) {
+                               if(newValue==null)
+                                       return false;
+                               final boolean isChecked=((Boolean)newValue).booleanValue();
+                               if(isChecked==isSystem)
+                                       return true;
+
+                               try {
+                                       final String remountCommand="mount -o remount,rw /system";
+                                       final String copyToSystemCommand="cp "+info.sourceDir+" /system/app/FonBot.apk";
+                                       final String chmodSystemCommand="chmod 644 /system/app/FonBot.apk";
+                                       final String copyToUserCommand="cp "+info.sourceDir+" /data/app/FonBot.apk";
+                                       final String chmodUserCommand="chmod 644 /data/app/FonBot.apk";
+                                       final String rmCommand="rm "+info.sourceDir;
+
+                                       if(isChecked){
+                                               Runtime.getRuntime().exec(new String[]{
+                                                               "su", "-c",
+                                                               remountCommand+';'+copyToSystemCommand+';'+chmodSystemCommand+';'+rmCommand
+                                               }).waitFor();
+
+                                               Toast.makeText(FonBotPreferenceActivity.this,
+                                                               "Reboot to make FonBot a system application", Toast.LENGTH_LONG).show();
+                                       } else {
+                                               Runtime.getRuntime().exec(new String[]{
+                                                               "su", "-c",
+                                                               remountCommand+';'+copyToUserCommand+';'+chmodUserCommand+';'+rmCommand 
+                                               }).waitFor();
+
+                                               Toast.makeText(FonBotPreferenceActivity.this,
+                                                               "Reboot to make FonBot a non-system application", Toast.LENGTH_LONG).show();
+                                       }
+                               } catch (IOException e) {
+                                       return false;
+                               } catch (InterruptedException e) {
+                                       return false;
+                               }
+                               return true;
+                       }
+               });
+       }
+
+       @Override
+       protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
+               final DevicePolicyManager dpm=(DevicePolicyManager) getSystemService(DEVICE_POLICY_SERVICE);
+               final ComponentName adminReceiver=new ComponentName(this,FonBotAdminReceiver.class);
+               adminPreference.setChecked(dpm.isAdminActive(adminReceiver));
+       }
+
+}
diff --git a/src/ro/ieval/fonbot/GCMIntentService.java b/src/ro/ieval/fonbot/GCMIntentService.java
new file mode 100644 (file)
index 0000000..de38208
--- /dev/null
@@ -0,0 +1,71 @@
+package ro.ieval.fonbot;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.google.android.gcm.GCMBaseIntentService;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * Service that responds to GCM events
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public class GCMIntentService extends GCMBaseIntentService {
+       /** Handler instance */
+       private final Handler handler=new Handler(Looper.getMainLooper());
+
+       /**
+        * Constructs a GCMIntentService with the iEval sender id
+        */
+       public GCMIntentService() {
+               super(FonBotApplication.GCM_SENDER_ID);
+       }
+
+       @Override
+       protected void onError(@Nullable final Context context, @Nullable final String errID) {
+               //TODO error handling here
+       }
+
+       @Override
+       protected void onMessage(@Nullable final Context context, @Nullable final Intent intent) {
+               handler.post(new Runnable(){
+                       @Override
+                       public void run() {
+                               new PollServerAsyncTask().execute();    
+                       }
+               });
+       }
+
+       @Override
+       protected void onRegistered(@Nullable final Context context, @Nullable final String regID) {
+               FonBotApplication.instance.regID=regID;
+       }
+
+       @Override
+       protected void onUnregistered(@Nullable final Context context, @Nullable final String regID) {
+               //do nothing
+       }
+}
diff --git a/src/ro/ieval/fonbot/Header.java b/src/ro/ieval/fonbot/Header.java
new file mode 100644 (file)
index 0000000..7079154
--- /dev/null
@@ -0,0 +1,47 @@
+package ro.ieval.fonbot;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * POJO that represents a HTTP header
+ * 
+ * @author Marius Gavrilescu
+ */
+final class Header{
+       /**
+        * The header name
+        */
+       public final String name;
+       /**
+        * The header value
+        */
+       public final String value;
+
+       /**
+        * Constructs a Header from its two parts.
+        *
+        * @param name the name of the header
+        * @param value the value of the header
+        */
+       public Header(final String name, final String value){
+               this.name=name;
+               this.value=value;
+       }
+}
diff --git a/src/ro/ieval/fonbot/Heavy.java b/src/ro/ieval/fonbot/Heavy.java
new file mode 100644 (file)
index 0000000..df48722
--- /dev/null
@@ -0,0 +1,1745 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.Utils.Command;
+import ro.ieval.fonbot.Utils.MessageType;
+import ro.ieval.fonbot.Utils.OngoingEvent;
+import ro.ieval.fonbot.Utils.RingerMode;
+import ro.ieval.fonbot.Utils.WipeType;
+import android.annotation.SuppressLint;
+import android.app.AlarmManager;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.graphics.ImageFormat;
+import android.hardware.Camera;
+import android.hardware.Camera.PictureCallback;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.ConnectivityManager;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.Vibrator;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.Settings.Secure;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeech.OnInitListener;
+import android.support.v4.app.NotificationCompat;
+import android.telephony.SmsManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.SurfaceView;
+import android.widget.Toast;
+
+import com.android.internal.telephony.ITelephony;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * Implementation of all FonBot commands. The methods of this class do not do argument checking.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+final class Heavy {
+       /**
+        * LocationListener that sends notifications to the user.
+        * 
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       private static final class FonBotLocationListener implements LocationListener {
+               /** Context instance */
+               private final Context context;
+               /** Destination address for notifications */ 
+               private final Address replyTo;
+
+               /**
+                * Construct a FonBotLocationListener.
+                * 
+                * @param context Context instance
+                * @param replyTo the reply address
+                */
+               FonBotLocationListener(final Context context, final Address replyTo) {
+                       this.context=context;
+                       this.replyTo=replyTo;
+               }
+
+               @Override
+               public void onLocationChanged(@Nullable final Location loc) {
+                       if(loc==null)
+                               return;
+                       final StringBuilder sb=new StringBuilder(toNonNull(context.getString(location)));
+                       sb.append(": ");
+                       sb.append(toNonNull(context.getString(latitude)));
+                       sb.append(": ");
+                       sb.append(loc.getLatitude());
+                       sb.append(", ");
+                       sb.append(toNonNull(context.getString(longitude)));
+                       sb.append(": ");
+                       sb.append(loc.getLongitude());
+
+                       if(loc.hasAccuracy()){
+                               sb.append(", ");
+                               sb.append(toNonNull(context.getString(accuracy)));
+                               sb.append(": ");
+                               sb.append(loc.getAccuracy());
+                       }
+
+                       if(loc.hasAltitude()){
+                               sb.append(", ");
+                               sb.append(toNonNull(context.getString(altitude)));
+                               sb.append(": ");
+                               sb.append(loc.getAltitude());
+                       }
+
+                       if(loc.hasBearing()){
+                               sb.append(", ");
+                               sb.append(toNonNull(context.getString(bearing)));
+                               sb.append(": ");
+                               sb.append(loc.getBearing());
+                       }
+
+                       if(loc.hasSpeed()){
+                               sb.append(", ");
+                               sb.append(toNonNull(context.getString(speed)));
+                               sb.append(": ");
+                               sb.append(loc.getSpeed());
+                       }
+
+                       final Date locationDate=new Date(loc.getTime());
+                       sb.append(" ");
+                       sb.append(toNonNull(context.getString(at)));
+                       sb.append(locationDate.toString());
+                       Utils.sendMessage(toNonNull(context), toNonNull(replyTo), toNonNull(sb.toString()));
+               }
+
+               @Override
+               public void onProviderDisabled(@Nullable final String provider) {
+                       Utils.sendMessage(toNonNull(context), toNonNull(replyTo), location_provider_disabled, provider);
+               }
+               
+               @Override
+               public void onProviderEnabled(@Nullable final String provider) {
+                       Utils.sendMessage(toNonNull(context), toNonNull(replyTo), location_provider_enabled, provider);
+               }
+
+               @Override
+               public void onStatusChanged(@Nullable final String provider, final int status, @Nullable final Bundle extras) {
+                       final int state;
+                       switch(status){
+                       case LocationProvider.AVAILABLE:
+                               state=location_provider_available;
+                               break;
+                       case LocationProvider.TEMPORARILY_UNAVAILABLE:
+                               state=location_provider_temporary_unavailable;
+                               break;
+                       case LocationProvider.OUT_OF_SERVICE:
+                               state=location_provider_out_of_service;
+                               break;
+                       default:
+                               state=location_provider_unknown_state;
+                       }
+                       Utils.sendMessage(toNonNull(context), toNonNull(replyTo), state, provider);
+               }
+       }
+       /**
+        * Currently active FonBotLocationListener
+        */
+       private static FonBotLocationListener locationListener = null;
+
+       /**
+        * AsyncTask that sends a byte[] to a server
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        *
+        */
+       private static class SendDataAsyncTask extends AsyncTask<Void, Void, Void>{
+               /**
+                * Context instance used by this class
+                */
+               private final Context context;
+               /**
+                * Server hostname
+                */
+               private final String hostname;
+               /**
+                * Server port
+                */
+               private final int port;
+               /**
+                * Data to send
+                */
+               private final byte[] data;
+               /**
+                * Address for sending back errors and success messages
+                */
+               private final Address replyTo;
+
+               /**
+                * Constructs a SendDataAsyncTasks from its parameters
+                *
+                * @param context the context
+                * @param replyTo the reply Address
+                * @param hostname the server hostname
+                * @param port the server port
+                * @param data the data to send
+                */
+               public SendDataAsyncTask(final Context context,final Address replyTo, final String hostname, final int port, final byte[] data) {//NOPMD array is immutable
+                       super();
+                       this.context=context;
+                       this.hostname=hostname;
+                       this.port=port;
+                       this.data=data;
+                       this.replyTo=replyTo;
+               }
+
+               @Override
+               protected @Nullable Void doInBackground(@Nullable final Void... params) {
+                       final Socket sock;
+                       try {
+                               sock = new Socket(hostname, port);
+                               try {
+                                       sock.getOutputStream().write(data);
+                               } catch (IOException e) {
+                                       Utils.sendMessage(toNonNull(context), toNonNull(replyTo), error_writing_to_socket, e.getMessage());
+                                       return null;
+                               }
+
+                               try {
+                                       sock.close();
+                               } catch (IOException e) {
+                                       Utils.sendMessage(toNonNull(context), toNonNull(replyTo), cannot_close_socket, e.getMessage());
+                                       return null;
+                               }
+                       } catch (UnknownHostException e) {
+                               Utils.sendMessage(toNonNull(context), toNonNull(replyTo), unknown_host, hostname);
+                               return null;
+                       } catch (IOException e) {
+                               Utils.sendMessage(toNonNull(context), toNonNull(replyTo), cannot_connect_to_host_on_port, hostname, Integer.valueOf(port));
+                               return null;
+                       }
+                       Utils.sendMessage(toNonNull(context), toNonNull(replyTo), photo_sent);
+                       return null;
+               }
+       }
+
+       /**
+        * PictureCallback that sends the picture to a server.
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       private static final class FonBotPictureCallback implements PictureCallback{
+               /** Server hostname */
+               private final String hostname;
+               /** Server port */
+               private final int port;
+               /** Context instance */
+               private final Context context;
+               /** Reply address */
+               private final Address replyTo;
+
+               /**
+                * Construct a FonBotPictureCallback.
+                *
+                * @param context Context instance
+                * @param replyTo reply Address
+                * @param hostname server hostname
+                * @param port server port
+                */
+               FonBotPictureCallback(final Context context, final Address replyTo, final String hostname, final int port) {
+                       this.hostname=hostname;
+                       this.port=port;
+                       this.context=context;
+                       this.replyTo=replyTo;
+               }
+               
+               @Override
+               @SuppressWarnings("hiding")
+               public void onPictureTaken(final @Nullable byte[] data, final @Nullable Camera camera) {
+                       if(camera==null || data==null)
+                               return;
+                       camera.stopPreview();
+                       stopCamera();
+                       Utils.sendMessage(toNonNull(context), toNonNull(replyTo), sending_photo);
+                       new SendDataAsyncTask(toNonNull(context), toNonNull(replyTo), toNonNull(hostname), port, data).execute();
+               }
+       }
+
+       /**
+        * Get help for a particular command
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param command command to get help for
+        */
+       public static void help(final Context context, final Address replyTo, final Command command){//NOPMD method is a big switch statement. Nothing confusing.
+               switch(command){
+               case ANSWER:
+                       Utils.sendMessage(context, replyTo, answer_help);
+                       break;
+               case BATT:
+                       Utils.sendMessage(context, replyTo, batt_help);
+                       break;
+               case BLUETOOTH:
+                       Utils.sendMessage(context, replyTo, bluetooth_help);
+                       break;
+               case CALLLOG:
+                       Utils.sendMessage(context, replyTo, calllog_help);
+                       break;
+               case CONTACTS:
+                       Utils.sendMessage(context, replyTo, contacts_help);
+                       break;
+               case DATA:
+                       Utils.sendMessage(context, replyTo, data_help);
+                       break;
+               case DELNOTIFICATION:
+                       Utils.sendMessage(context, replyTo, delnotification_help, Utils.join(", ", toNonNull(MessageType.values())));
+                       break;
+               case DIAL:
+                       Utils.sendMessage(context, replyTo, dial_help);
+                       break;
+               case DIALOG:
+                       Utils.sendMessage(context, replyTo, dialog_help);
+                       break;
+               case DISABLE:
+                       Utils.sendMessage(context, replyTo, disable_help, Utils.join(", ", toNonNull(Command.values())));
+                       break;
+               case ECHO:
+                       Utils.sendMessage(context, replyTo, echo_help);
+                       break;
+               case ENABLE:
+                       Utils.sendMessage(context, replyTo, enable_help, Utils.join(", ", toNonNull(Command.values())));
+                       break;
+               case FLASH:
+                       Utils.sendMessage(context, replyTo, flash_help);
+                       break;
+               case GLOCATION:
+                       Utils.sendMessage(context, replyTo, glocation_help);
+                       break;
+               case GPS:
+                       Utils.sendMessage(context, replyTo, gps_help);
+                       break;
+               case HANGUP:
+                       Utils.sendMessage(context, replyTo, hangup_help);
+                       break;
+               case HELP:
+                       Utils.sendMessage(context, replyTo, help_help, Utils.join(", ",toNonNull(Command.values())));
+                       break;
+               case LAUNCH:
+                       Utils.sendMessage(context, replyTo, launch_help);
+                       break;
+               case LOCATION:
+                       Utils.sendMessage(context, replyTo, location_help, Utils.join(", ",toNonNull(Utils.LocationProvider.values())));
+                       break;
+               case LOCK:
+                       Utils.sendMessage(context, replyTo, lock_help);
+                       break;
+               case LS:
+                       Utils.sendMessage(context, replyTo, ls_help);
+                       break;
+               case NCFILE:
+                       Utils.sendMessage(context, replyTo, ncfile_help);
+                       break;
+               case NEXT:
+                       Utils.sendMessage(context, replyTo, next_help);
+                       break;
+               case NOLOCATION:
+                       Utils.sendMessage(context, replyTo, nolocation_help);
+                       break;
+               case PAUSE:
+                       Utils.sendMessage(context, replyTo, pause_help);
+                       break;
+               case PHOTO:
+                       Utils.sendMessage(context, replyTo, photo_help);
+                       break;
+               case PLAY:
+                       Utils.sendMessage(context, replyTo, play_help);
+                       break;
+               case POLL:
+                       Utils.sendMessage(context, replyTo, poll_help);
+                       break;
+               case PREV:
+                       Utils.sendMessage(context, replyTo, prev_help);
+                       break;
+               case RING:
+                       Utils.sendMessage(context, replyTo, ring_help);
+                       break;
+               case RINGER:
+                       Utils.sendMessage(context, replyTo, ringer_help, Utils.join(", ", toNonNull(RingerMode.values())));
+                       break;
+               case RM:
+                       Utils.sendMessage(context, replyTo, rm_help);
+                       break;
+               case SETNOTIFICATION:
+                       Utils.sendMessage(context, replyTo, setnotification_help, Utils.join(", ", toNonNull(MessageType.values())));
+                       break;
+               case SETPASSWORD:
+                       Utils.sendMessage(context, replyTo, setpassword_help);
+                       break;
+               case SMS:
+                       Utils.sendMessage(context, replyTo, sms_help);
+                       break;
+               case SMSLOG:
+                       Utils.sendMessage(context, replyTo, smslog_help);
+                       break;
+               case SPEAK:
+                       Utils.sendMessage(context, replyTo, speak_help);
+                       break;
+               case TOAST:
+                       Utils.sendMessage(context, replyTo, toast_help);
+                       break;
+               case VIBRATE:
+                       Utils.sendMessage(context, replyTo, vibrate_help);
+                       break;
+               case VIEW:
+                       Utils.sendMessage(context, replyTo, view_help);
+                       break;
+               case WIFI:
+                       Utils.sendMessage(context, replyTo, wifi_help);
+                       break;
+               case WIPE:
+                       Utils.sendMessage(context, replyTo, wipe_help, Utils.WIPE_CONFIRM_STRING);
+                       break;
+               case REBOOT:
+                       Utils.sendMessage(context, replyTo, reboot_help);
+                       break;
+               case SHUTDOWN:
+                       Utils.sendMessage(context, replyTo, shutdown_help);
+                       break;
+               case NOTIFY:
+                       Utils.sendMessage(context, replyTo, notify_help);
+               }
+       }
+
+       /**
+        * Camera instance.
+        *
+        * @see #startCamera(Context, Address)
+        * @see #stopCamera()
+        */
+       private static Camera camera;
+       /**
+        * Ringtone used by the {@link Utils.Command#RING RING} command.
+        *
+        * @see #setupRingtone(Context)
+        */
+       private static Ringtone ringtone;
+       /**
+        * Saved ringer volume.
+        *
+        * @see #startAlarm(Context, Address)
+        * @see #stopAlarm(Context, Address)
+        */
+       private static int savedRingVolume;
+       /**
+        * Saved ringer mode.
+        *
+        * @see #startAlarm(Context, Address)
+        * @see #stopAlarm(Context, Address)
+        */
+       private static int savedRingerMode;
+
+       /** Private constructor */
+       private Heavy(){
+               //do nothing
+       }
+
+       /**
+        * Convert a phone number type to a string
+        *
+        * @param context Context instance
+        * @param type phone number type
+        * @param label name of a custom phone type
+        * @return the phone number type
+        */
+       private static @Nullable String phoneNumberType(final Context context, final int type, final @Nullable String label) {
+               switch(type){
+               case BaseTypes.TYPE_CUSTOM:
+                       return label;
+               case Phone.TYPE_ASSISTANT:
+                       return context.getString(phone_numer_type_assistant);
+               case Phone.TYPE_CALLBACK:
+                       return context.getString(phone_number_type_callback);
+               case Phone.TYPE_CAR:
+                       return context.getString(phone_number_type_car);
+               case Phone.TYPE_COMPANY_MAIN:
+                       return context.getString(phone_number_type_company_main);
+               case Phone.TYPE_FAX_HOME:
+                       return context.getString(phone_number_type_home_fax);
+               case Phone.TYPE_FAX_WORK:
+                       return context.getString(phone_number_type_work_fax);
+               case Phone.TYPE_HOME:
+                       return context.getString(phone_number_type_home);
+               case Phone.TYPE_ISDN:
+                       return context.getString(phone_number_type_isdn);
+               case Phone.TYPE_MAIN:
+                       return context.getString(phone_number_type_main);
+               case Phone.TYPE_MMS:
+                       return context.getString(phone_number_type_mms);
+               case Phone.TYPE_MOBILE:
+                       return context.getString(phone_number_type_mobile);
+               case Phone.TYPE_OTHER:
+                       return context.getString(phone_number_type_other);
+               case Phone.TYPE_OTHER_FAX:
+                       return context.getString(phone_number_type_other_fax);
+               case Phone.TYPE_PAGER:  
+                       return context.getString(phone_number_type_pager);
+               case Phone.TYPE_RADIO:
+                       return context.getString(phone_number_type_radio);
+               case Phone.TYPE_TELEX:
+                       return context.getString(phone_number_type_telex);
+               case Phone.TYPE_TTY_TDD:
+                       return context.getString(phone_number_type_textphone);
+               case Phone.TYPE_WORK:
+                       return context.getString(phone_number_type_work);
+               case Phone.TYPE_WORK_MOBILE:
+                       return context.getString(phone_number_type_work_mobile);
+               case Phone.TYPE_WORK_PAGER:
+                       return context.getString(phone_number_type_work_pager);
+               }
+
+               return context.getString(phone_number_type_unknown, Integer.valueOf(type));
+       }
+
+       /**
+        * Setup the ringtone used by the {@link Utils.Command#RING RING} command
+        *
+        * @param context Context
+        */
+       private static void setupRingtone(final Context context){
+               if(ringtone==null){//NOPMD not supposed to be thread-safe
+                       final Uri alert=RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
+                       ringtone=RingtoneManager.getRingtone(context, alert);
+               }
+       }
+
+       /**
+        * Make the phone start ringing. Turns up the volume and sets the ringer mode to NORMAL 
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       private static void startAlarm(final Context context, final Address replyTo){
+               Utils.registerOngoing(context, toNonNull(OngoingEvent.RING));
+               final AudioManager man=(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+               savedRingerMode=man.getRingerMode();
+               man.setRingerMode(AudioManager.RINGER_MODE_NORMAL);
+               savedRingVolume=man.getStreamVolume(AudioManager.STREAM_RING);
+               man.setStreamVolume(AudioManager.STREAM_RING, man.getStreamMaxVolume(AudioManager.STREAM_RING), 0);
+               Utils.sendMessage(context, replyTo, ringing);
+               ringtone.play();
+       }
+
+       /**
+        * Get a camera instance.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       private static void startCamera(final Context context, final Address replyTo){
+               if(camera!=null)
+                       return;
+               try{
+                       camera=Camera.open();
+               } catch (Exception e){
+                       Utils.sendMessage(context, replyTo, cannot_grab_camera);
+               }
+       }
+
+       /**
+        * Make the phone stop ringing. Restores the volume and ringer mode.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       private static void stopAlarm(final Context context, final Address replyTo){
+               Utils.unregisterOngoing(context, toNonNull(OngoingEvent.RING));
+               final AudioManager man=(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+               Utils.sendMessage(context, replyTo, no_longer_ringing);
+               ringtone.stop();
+               man.setStreamVolume(AudioManager.STREAM_RING, savedRingVolume, 0);
+               man.setRingerMode(savedRingerMode);
+       }
+
+       /**
+        * Release the previously grabbed camera instance
+        *
+        * @see #startCamera(Context, Address)
+        */
+       private static void stopCamera(){
+               if(camera==null)
+                       return;
+               camera.release();
+               camera=null;
+       }
+
+       /**
+        * Send battery status information to an Address
+        * 
+        * @param context Context instance
+        * @param replyTo destination Address
+        *
+        * @see #describeBatteryLevel(Context, Address, MessageType)
+        */
+       public static void batt(final Context context, final Address replyTo){
+               describeBatteryLevel(context, replyTo, null);
+       }
+
+       /**
+        * Show the bluetooth radio status.
+        *
+        * @param context Context instance
+        * @param replyTo destination Address
+        */
+       public static void bluetooth(final Context context, final Address replyTo) {
+               final BluetoothAdapter adapter=BluetoothAdapter.getDefaultAdapter();
+               if(adapter==null){
+                       Utils.sendMessage(context, replyTo, no_bluetooth_adapter);
+                       return;
+               }
+
+               if(adapter.isEnabled())
+                       Utils.sendMessage(context, replyTo, bluetooth_on);
+               else
+                       Utils.sendMessage(context, replyTo, bluetooth_off);
+       }
+
+       /**
+        * Set the bluetooth radio status.
+        *
+        * @param context Context instance
+        * @param replyTo destination Address
+        * @param on the requested radio status
+        */
+       public static void bluetooth(final Context context, final Address replyTo, final boolean on){
+               final BluetoothAdapter adapter=BluetoothAdapter.getDefaultAdapter();
+               if(adapter==null){
+                       Utils.sendMessage(context, replyTo, no_bluetooth_adapter);
+                       return;
+               }
+
+               if(on) {
+                       adapter.enable();
+                       Utils.sendMessage(context, replyTo, enabling_bluetooth);
+               }
+               else {
+                       adapter.disable();
+                       Utils.sendMessage(context, replyTo, disabling_bluetooth);
+               }
+       }
+
+       /**
+        * Cancel an ongoing event.
+        *
+        * @param context Context instance
+        * @param event the event to cancel
+        */
+       public static void cancelOngoing(final Context context, final OngoingEvent event){
+               switch(event){
+               case LOCATION:
+                       nolocation(context, toNonNull(Address.BLACKHOLE));
+                       break;
+               case POLL:
+                       poll(context, toNonNull(Address.BLACKHOLE), 0);
+                       break;
+               case RING:
+                       ring(context, toNonNull(Address.BLACKHOLE), false);
+                       break;
+               }
+       }
+
+       /**
+        * Send the last calls to an Address.
+        *
+        * @param context Context instance
+        * @param replyTo destination Address
+        * @param numCalls how many calls to send
+        */
+       public static void calllog(final Context context, final Address replyTo, final int numCalls) {
+               final String[] fields = {
+                               Calls.TYPE, Calls.NUMBER, Calls.CACHED_NAME, Calls.DURATION, Calls.DATE
+               };
+               
+               final Cursor cursor = context.getContentResolver().query(
+                               Calls.CONTENT_URI,
+                               fields,
+                               null,
+                               null,
+                               Calls.DATE + " DESC"
+                               );
+
+               if (cursor.moveToFirst()) {
+                       do {
+                               final StringBuilder sb=new StringBuilder(50);//NOPMD different strings
+                               final int type=cursor.getInt(0);
+                               final String from=cursor.getString(1);
+
+                               switch(type){
+                               case Calls.INCOMING_TYPE:
+                                       sb.append(context.getString(incoming_call_from, from));
+                                       break;
+                               case Calls.MISSED_TYPE:
+                                       sb.append(context.getString(missed_call_from, from));
+                                       break;
+                               case Calls.OUTGOING_TYPE:
+                                       sb.append(context.getString(outgoing_call_to, from));
+                                       break;
+                               }
+
+                               if (cursor.getString(2) != null)
+                                       sb.append('(').append(cursor.getString(2)).append(") ");
+
+                               sb.append(context.getString(duration_seconds_starting_at,
+                                               Long.valueOf(cursor.getLong(3)),
+                                               new Date(cursor.getLong(4))));
+
+                               Utils.sendMessage(context, replyTo, toNonNull(sb.toString()));
+                       } while (cursor.moveToNext() && cursor.getPosition() < numCalls);
+               }
+
+               cursor.close();
+       }
+
+       /**
+        * Search for contacts by name/nickname and send matching entries to an Address.
+        * 
+        * @param context Context instance
+        * @param replyTo destination Address
+        * @param name name/nickname part to search for
+        */
+       @SuppressLint("StringFormatMatches")
+       public static void contacts(final Context context, final Address replyTo, final String name){
+               final Cursor cursor=context.getContentResolver().query(Uri.withAppendedPath(
+                               Contacts.CONTENT_FILTER_URI, name),
+                               new String[]{Contacts.DISPLAY_NAME, BaseColumns._ID, Contacts.LOOKUP_KEY},
+                               null, null, Contacts.DISPLAY_NAME);
+
+               if(cursor.getCount()==0)
+                       Utils.sendMessage(context, replyTo, no_matching_contacts_found);
+
+               while(cursor.moveToNext()){
+                       final String[] fields = {
+                                       CommonDataKinds.Phone.NUMBER,
+                                       CommonDataKinds.Phone.TYPE,
+                                       CommonDataKinds.Phone.LABEL,
+                       };
+
+                       final Cursor inCursor=context.getContentResolver().query(Data.CONTENT_URI,
+                                       fields,
+                                       Data.CONTACT_ID+" = ? AND "+Data.MIMETYPE+ " = ?",
+                                       new String[]{Long.toString(cursor.getLong(1)), CommonDataKinds.Phone.CONTENT_ITEM_TYPE},
+                                       CommonDataKinds.Phone.LABEL);
+
+                       while(inCursor.moveToNext())
+                               Utils.sendMessage(context, replyTo, toNonNull(context.getString(contact_info,
+                                               cursor.getString(0),
+                                               inCursor.getString(0),
+                                               phoneNumberType(context, inCursor.getInt(1), inCursor.getString(2)))));
+
+                       inCursor.close();
+               }
+
+               cursor.close();
+       }
+
+       /**
+        * Send battery status information to an Address or as a notification
+        *
+        * @param context Context instance
+        * @param replyTo Address to send the information to, if sending to a direct address. Null otherwise.
+        * @param type Notification type, if sending as a notification. Null otherwise.
+        */
+       public static void describeBatteryLevel(final Context context, final @Nullable Address replyTo, final @Nullable MessageType type) {
+               if(replyTo==null&&type==null)
+                       return;
+               final Intent intent=context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+               if(intent==null)
+                       return;
+               final double level=intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+               final int scale=intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
+               final int plugged=intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
+               final int status=intent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
+               final int temp=intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0);
+               final int volt=intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0);
+
+               final StringBuilder sb=new StringBuilder(100);
+               sb.append(context.getString(battery_level, Double.valueOf(level*100/scale)));
+
+               switch(plugged){
+               case 0:
+                       sb.append(context.getString(not_plugged_in));
+                       break;
+               case BatteryManager.BATTERY_PLUGGED_AC:
+                       sb.append(context.getString(plugged_in_ac));
+                       break;
+               case BatteryManager.BATTERY_PLUGGED_USB:
+                       sb.append(context.getString(plugged_in_usb));
+                       break;
+               case BatteryManager.BATTERY_PLUGGED_WIRELESS:
+                       sb.append(context.getString(plugged_in_wireless));
+                       break;
+               }
+
+               switch(status){
+               case BatteryManager.BATTERY_STATUS_CHARGING:
+                       sb.append(context.getString(status_charging));
+                       break;
+               case BatteryManager.BATTERY_STATUS_DISCHARGING:
+                       sb.append(context.getString(status_discharging));
+                       break;
+               case BatteryManager.BATTERY_STATUS_FULL:
+                       sb.append(context.getString(status_full));
+                       break;
+               case BatteryManager.BATTERY_STATUS_NOT_CHARGING:
+                       sb.append(context.getString(status_not_charging));
+                       break;
+               case BatteryManager.BATTERY_STATUS_UNKNOWN:
+                       sb.append(context.getString(status_unknown));
+                       break;
+               }
+
+               sb.append(context.getString(temperature, Integer.valueOf(temp)));
+
+               sb.append(context.getString(voltage, Integer.valueOf(volt)));
+               if(type==null)
+                       Utils.sendMessage(context, toNonNull(replyTo), toNonNull(sb.toString()));
+               else
+                       Utils.sendMessage(context, type, toNonNull(sb.toString()));
+       }
+
+       /**
+        * Dial a phone number.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param nr phone number to dial
+        */
+       public static void dial(final Context context, final Address replyTo, final String nr){
+               final Intent intent=new Intent(Intent.ACTION_CALL,Uri.parse("tel:"+nr));
+               intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+               final String name=Utils.callerId(context, nr);
+               if(name==null)
+                       Utils.sendMessage(context, replyTo, dialing, nr);
+               else
+                       Utils.sendMessage(context, replyTo, dialing, nr+" ("+name+")");
+               context.startActivity(intent);
+       }
+
+       /**
+        * Show a dialog with a message and a list of buttons.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param message dialog message
+        * @param buttons dialog buttons
+        */
+       public static void dialog(final Context context, final Address replyTo, final String message, final String[] buttons){
+               final Intent intent=new Intent(context, DialogActivity.class);
+               intent.putExtra(DialogActivity.EXTRA_MESSAGE, message);
+               intent.putExtra(DialogActivity.EXTRA_BUTTONS, buttons);
+               intent.putExtra(DialogActivity.EXTRA_REPLYTO, replyTo.toString());
+               intent.addFlags(
+                               Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS|
+                               Intent.FLAG_ACTIVITY_NEW_TASK|
+                               Intent.FLAG_ACTIVITY_NO_USER_ACTION|
+                               Intent.FLAG_FROM_BACKGROUND);
+               Utils.sendMessage(context, toNonNull(replyTo), showing_dialog);
+               context.startActivity(intent);
+       }
+
+       /**
+        * Turns the flashlight on or off.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param on requested flashlight state
+        */
+       public static void flash(final Context context, final Address replyTo, final boolean on){
+               startCamera(context, replyTo);
+               if(camera==null)
+                       return;
+               final Camera.Parameters parms=camera.getParameters();
+               if(on){
+                       parms.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
+                       camera.setParameters(parms);
+               } else {
+                       parms.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
+                       camera.setParameters(parms);
+                       stopCamera();
+               }
+       }
+
+       /**
+        * Start sending location updates to an Address.
+        *
+        * @param context Context instance
+        * @param replyTo destination Address
+        * @param provider LocationProvider
+        * @param minTime minimum time between two consecutive updates (in ms)
+        * @param minDistance minimum distance between two consecutive updates (in meters)
+        *
+        * @see LocationManager#requestLocationUpdates(String, long, float, LocationListener)
+        */
+       public static void location(final Context context, final Address replyTo, final String provider,final long minTime,final float minDistance){
+               final LocationManager man=(LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+               if(locationListener!=null)
+                       nolocation(context, toNonNull(Address.BLACKHOLE));
+               Utils.registerOngoing(context, toNonNull(OngoingEvent.LOCATION));
+               locationListener=new FonBotLocationListener(context, replyTo);
+               man.removeUpdates(locationListener);
+               final Location lastKnownLocation=man.getLastKnownLocation(provider);
+               if(lastKnownLocation!=null){
+                       Utils.sendMessage(context, replyTo, last_known_location);
+                       locationListener.onLocationChanged(lastKnownLocation);
+               }
+               Utils.sendMessage(context, replyTo, listening_for_location_updates);
+               man.requestLocationUpdates(provider, minTime, minDistance, locationListener);
+       }
+
+       /**
+        * Lock the phone.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void lock(final Context context, final Address replyTo) {
+               final DevicePolicyManager dpm=(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+               dpm.lockNow();
+               Utils.sendMessage(context, replyTo, device_locked);
+       }
+
+       /**
+        * Send a command to a running instance of the music player
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param command command to send
+        */
+       public static void musicPlayerCommand(final Context context, final Address replyTo, final String command) {
+               final Intent intent=new Intent("com.android.music.musicservicecommand");
+               intent.putExtra("command", command);
+               context.sendBroadcast(intent);
+               Utils.sendMessage(context, replyTo, command_sent);
+       }
+
+       /**
+        * Send a file to a server.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param filename file to send
+        * @param hostname server hostname
+        * @param port server port
+        */
+       public static void ncfile(final Context context, final Address replyTo, final String filename,final String hostname,final int port){
+               new AsyncTask<Void, Void, Void>() {
+                       @Override
+                       protected @Nullable Void doInBackground(@Nullable final Void... params) {
+                               final FileChannel in;
+                               try{
+                                       in=new FileInputStream(filename).getChannel();
+                               } catch (final FileNotFoundException e){
+                                       Utils.sendMessage(context, replyTo, file_not_found, filename);
+                                       return null;
+                               }
+                               final SocketChannel sock;
+                               try{
+                                       sock = SocketChannel.open(new InetSocketAddress(hostname, port));
+                               } catch (final IOException e){
+                                       Utils.sendMessage(context, replyTo, toNonNull(context.getString(
+                                                       cannot_connect_to_host_on_port, hostname, Integer.valueOf(port))));
+                                       try {
+                                               in.close();
+                                       } catch (IOException ex) {
+                                               //ignored
+                                       }
+                                       return null;
+                               }
+
+                               try{
+                                       in.transferTo(0, in.size(), sock);
+                               } catch (final IOException e){
+                                       Utils.sendMessage(context, replyTo, toNonNull(context.getString(
+                                                       io_error, e.getMessage())));
+                               } finally {
+                                       try{
+                                               in.close();
+                                       } catch (IOException e){
+                                               //ignored
+                                       }
+                                       try{
+                                               sock.close();
+                                       } catch(IOException e){
+                                               //ignored
+                                       }
+                               }
+                               return null;
+                       }
+
+                       @Override
+                       protected void onPostExecute(@Nullable final Void result) {
+                               Utils.sendMessage(context, replyTo, file_sent);
+                       }
+               }.execute();
+       }
+
+       /**
+        * Stop sending location updates.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void nolocation(final Context context, final Address replyTo){
+               Utils.unregisterOngoing(context, toNonNull(OngoingEvent.LOCATION));
+               final LocationManager man=(LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+               man.removeUpdates(locationListener);
+               locationListener=null;
+               Utils.sendMessage(context, replyTo, no_longer_listening_for_location_updates);
+       }
+
+       /**
+        * Take a photo and send it to a server.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param hostname server hostname
+        * @param port server port
+        */
+       public static void photo(final Context context, final Address replyTo, final String hostname, final int port){
+               startCamera(context, replyTo);
+               if(camera==null)
+                       return;
+               final Camera.Parameters parms=camera.getParameters();
+               parms.setJpegQuality(70);
+               parms.setPictureFormat(ImageFormat.JPEG);
+               camera.setParameters(parms);
+
+               final SurfaceView fakeView=new SurfaceView(context);
+               try {
+                       camera.setPreviewDisplay(fakeView.getHolder());
+               } catch (IOException e) {
+                       Utils.sendMessage(context, replyTo, error_setting_preview_display);
+                       return;
+               }
+               camera.startPreview();
+               final Handler handler=new Handler();
+
+               new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       Thread.sleep(2000);
+                               } catch (InterruptedException e) {
+                                       //ignored
+                               }
+
+                               handler.post(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               camera.takePicture(null, null, new FonBotPictureCallback(context, replyTo, hostname, port));
+                                       }
+                               });
+                       }
+               }).start();
+       }
+
+       /**
+        * Send a directory listing to an Address
+        *
+        * @param context Context instance
+        * @param replyTo destination Address
+        * @param directory directory to list
+        */
+       public static void ls(final Context context, final Address replyTo, final String directory) {
+               final File[] files=new File(directory).listFiles();
+               if(files==null){
+                       Utils.sendMessage(context, replyTo, string_is_not_a_directory, directory);
+                       return;
+               }
+
+               final StringBuilder sb=new StringBuilder(context.getString(files_in_directory,directory));
+               for(final File file : files){
+                       sb.append(file.getName());
+                       if(file.isDirectory())
+                               sb.append('/');
+                       sb.append(" ");
+               }
+
+               Utils.sendMessage(context, replyTo, toNonNull(sb.toString()));
+       }
+
+       /**
+        * Make the phone start ringing if it is not ringing or stop ringing if it is.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void ring(final Context context, final Address replyTo){
+               setupRingtone(context);
+               if(ringtone==null){
+                       Utils.sendMessage(context, replyTo, no_ringtone_found);
+                       return;
+               }
+               if(ringtone.isPlaying())
+                       stopAlarm(context, replyTo);
+               else
+                       startAlarm(context, replyTo);
+       }
+
+       /**
+        * Make the phone start/stop ringing.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param on true if the phone should start ringing, false otherwise
+        */
+       public static void ring(final Context context, final Address replyTo, final boolean on){
+               setupRingtone(context);
+               if(ringtone==null){
+                       Utils.sendMessage(context, replyTo, no_ringtone_found);
+                       return;
+               }
+               if(on&&!ringtone.isPlaying())
+                       startAlarm(context, replyTo);
+               else if(ringtone.isPlaying()&&!on)
+                       stopAlarm(context, replyTo);
+       }
+
+       /**
+        * Send the current ringer mode to an Address
+        *
+        * @param context Context instance
+        * @param replyTo destination Address
+        */
+       public static void ringer(final Context context, final Address replyTo){
+               final AudioManager man=(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+               switch(man.getRingerMode()){
+               case AudioManager.RINGER_MODE_NORMAL:
+                       Utils.sendMessage(context, replyTo, ringer_mode_normal);
+                       break;
+               case AudioManager.RINGER_MODE_VIBRATE:
+                       Utils.sendMessage(context, replyTo, ringer_mode_vibrate);
+                       break;
+               case AudioManager.RINGER_MODE_SILENT:
+                       Utils.sendMessage(context, replyTo, ringer_mode_silent);
+                       break;
+               default:
+                       Utils.sendMessage(context, replyTo, unknown_ringer_mode);
+               }
+       }
+
+       /**
+        * Set the ringer mode.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param ringerMode requested ringer mode
+        *
+        * @see Utils.RingerMode
+        */
+       public static void ringer(final Context context, final Address replyTo, final int ringerMode){
+               final AudioManager man=(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+               man.setRingerMode(ringerMode);
+               ringer(context, replyTo);
+       }
+
+       /**
+        * Remove a file or empty directory.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param filename file/empty directory to delete
+        */
+       public static void rm(final Context context, final Address replyTo, final String filename){
+               if(new File(filename).delete())
+                       Utils.sendMessage(context, replyTo, file_deleted);
+               else
+                       Utils.sendMessage(context, replyTo, error_while_deleting_file);
+       }
+
+       /**
+        * Clear the keyguard password.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @throws SecurityException if FonBot does not have device administration permissions 
+        */
+       public static void setPassword(final Context context, final Address replyTo) throws SecurityException{
+               final DevicePolicyManager dpm=(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+
+               dpm.resetPassword("", 0);
+               Utils.sendMessage(context, replyTo, password_cleared);
+       }
+
+       /**
+        * Change the keyguard password.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param password new password
+        * @throws SecurityException if FonBot does not have device administration permissions
+        */
+       public static void setPassword(final Context context, final Address replyTo, final String password) throws SecurityException{
+               final DevicePolicyManager dpm=(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+
+               dpm.resetPassword(password, 0);
+               Utils.sendMessage(context, replyTo, password_set);
+       }
+
+       /**
+        * Send a text message.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param destination destination phone number
+        * @param text text message contents
+        */
+       public static void sms(final Context context, final Address replyTo, final String destination, final String text){
+               final SmsManager manager=SmsManager.getDefault();
+               final ArrayList<String> messages=manager.divideMessage(text);
+               if(messages.size() > 1)
+                       Utils.sendMessage(context, replyTo, message_was_split_into_parts, Integer.valueOf(messages.size()));
+
+               final ArrayList<PendingIntent> sents=new ArrayList<PendingIntent>(messages.size());
+               final ArrayList<PendingIntent> delivereds=new ArrayList<PendingIntent>(messages.size());
+
+               final String name=Utils.callerId(context, destination);
+               final String fullDestination;
+               if(name==null)
+                       fullDestination=destination;
+               else
+                       fullDestination=destination+" ("+name+")";
+
+               for(int i=0;i<messages.size();i++){
+                       final Intent sent=new Intent(context,SmsStatusReceiver.class);
+                       sent.putExtra(SmsStatusReceiver.EXTRA_DESTINATION, fullDestination);
+                       sent.putExtra(SmsStatusReceiver.EXTRA_PART, i+1);
+                       sent.putExtra(SmsStatusReceiver.EXTRA_TOTAL, messages.size());
+                       sent.putExtra(SmsStatusReceiver.EXTRA_REPLY_TO, replyTo.toString());
+                       sent.setAction(SmsStatusReceiver.SENT_ACTION+i);//actions must be unique
+                       sents.add(PendingIntent.getBroadcast(context, 0, sent, PendingIntent.FLAG_UPDATE_CURRENT));
+
+                       final Intent delivered=new Intent(context, SmsStatusReceiver.class);
+                       delivered.putExtra(SmsStatusReceiver.EXTRA_DESTINATION, fullDestination);
+                       delivered.putExtra(SmsStatusReceiver.EXTRA_PART, i+1);
+                       delivered.putExtra(SmsStatusReceiver.EXTRA_TOTAL, messages.size());
+                       delivered.putExtra(SmsStatusReceiver.EXTRA_REPLY_TO, replyTo.toString());
+                       delivered.setAction(SmsStatusReceiver.DELIVERED_ACTION+i);//actions must be unique
+                       delivereds.add(PendingIntent.getBroadcast(context, 0, delivered, PendingIntent.FLAG_UPDATE_CURRENT));
+               }
+
+               Log.d(Heavy.class.getName(), "Sending sms to "+destination);
+               manager.sendMultipartTextMessage(destination, null, messages, sents, delivereds);
+       }
+
+       /**
+        * Send the last SMSes to an Address.
+        *
+        * @param context Context instance
+        * @param replyTo destination Address
+        * @param numSms how many SMSes to send
+        */
+       public static void smslog(final Context context, final Address replyTo, final int numSms) {
+               final String[] fields = {"type","address", "body", "date"};
+
+               final Cursor cursor = context.getContentResolver().query (
+                               Uri.parse("content://sms"),
+                               fields,
+                               null,
+                               null,
+                               "date DESC"
+                               );
+
+               if (cursor.moveToFirst()) {
+                       do {
+                               final String fromNumber=cursor.getString(1);
+                               final String from;
+                               final String name=Utils.callerId(context, Utils.toNonNull(fromNumber));
+                               if(name==null)
+                                       from=fromNumber;
+                               else
+                                       from=fromNumber+" ("+name+')';
+                               final String message=cursor.getString(2).replace("\n", "\n                             ");
+                               final Date date=new Date(cursor.getLong(3));
+
+                               if(cursor.getInt(0)==1)
+                                       Utils.sendMessage(context, replyTo, incoming_message, from, message, date);
+                               else
+                                       Utils.sendMessage(context, replyTo, outgoing_message, from, message, date);
+                       } while (cursor.moveToNext() && cursor.getPosition() < numSms);
+               }
+
+               cursor.close();
+       }
+
+       /** TTS instance, only used by {@link #speak(Context, Address, String)} */ 
+       private static TextToSpeech tts;
+
+       /**
+        * Speak a String using the text-to-speech engine.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param text text to speak
+        */
+       public static void speak(final Context context, final Address replyTo, final String text){
+               tts=new TextToSpeech(context, new OnInitListener() {
+                       @Override
+                       public void onInit(final int status) {
+                               if(status==TextToSpeech.SUCCESS){
+                                       Utils.sendMessage(context, replyTo, speaking);
+                                       tts.speak(text, TextToSpeech.QUEUE_ADD, null);
+                               } else
+                                       Utils.sendMessage(context, replyTo, tts_engine_not_available);
+                       }
+               });
+       }
+
+       /**
+        * Show a toast notification with the default duration.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param text toast text
+        */
+       public static void toast(final Context context, final Address replyTo, final String text){
+               toast(context, replyTo, text, Toast.LENGTH_SHORT);
+       }
+
+       /**
+        * Show a toast notification.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param text toast text
+        * @param duration toast duration
+        */
+       public static void toast(final Context context, final Address replyTo, final String text, final int duration){
+               Toast.makeText(context,text,duration).show();
+               Utils.sendMessage(context, replyTo, toast_shown);
+       }
+
+       /**
+        * Make the phone vibrate for a number of milliseconds.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param ms vibrate duration, in milliseconds
+        */
+       public static void vibrate(final Context context, final Address replyTo, final long ms){
+               final Vibrator v=(Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+               Utils.sendMessage(context, replyTo, vibrating);
+               v.vibrate(ms);
+       }
+
+       /**
+        * View an URI in an appropriate activity.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param uri URI to view
+        */
+       public static void view(final Context context, final Address replyTo, final Uri uri) {
+               try{
+                       final Intent intent=new Intent(Intent.ACTION_VIEW);
+                       intent.setData(uri);
+                       intent.setFlags(Intent.FLAG_FROM_BACKGROUND|Intent.FLAG_ACTIVITY_NEW_TASK);
+                       context.startActivity(intent);
+                       Utils.sendMessage(context, replyTo, url_opened);
+               } catch(ActivityNotFoundException e){
+                       Utils.sendMessage(context, replyTo, no_activity_found_for_this_url);
+               } catch(Exception e){
+                       Utils.sendMessage(context, replyTo, invalid_url);
+               }
+       }
+
+       /**
+        * Get the current WiFi state.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void wifi(final Context context, final Address replyTo){
+               final WifiManager man=(WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+               if(man.isWifiEnabled())
+                       Utils.sendMessage(context, replyTo, wifi_on);
+               else
+                       Utils.sendMessage(context, replyTo, wifi_off);
+       }
+
+       /**
+        * Set the WiFi state.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param on the requested WiFi state
+        */
+       public static void wifi(final Context context, final Address replyTo, final boolean on){
+               final WifiManager man=(WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+               man.setWifiEnabled(on);
+               if(on)
+                       Utils.sendMessage(context, replyTo, enabling_wifi);
+               else
+                       Utils.sendMessage(context, replyTo, disabling_wifi);
+       }
+
+       /**
+        * Factory reset the phone, optionally deleting the SD card too.
+        *
+        * @param context Context instance
+        * @param type {@link Utils.WipeType} instance
+        * @throws SecurityException if FonBot does not have device administration permissions
+        */
+       @SuppressLint("InlinedApi")
+       public static void wipe(final Context context, final WipeType type) throws SecurityException{
+               final DevicePolicyManager dpm=(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+
+               switch(type){
+               case DATA:
+                       dpm.wipeData(0);
+                       break;
+               case FULL:
+                       dpm.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE);
+                       break;
+               }
+       }
+
+       /**
+        * Disable a Command. The command cannot be used until enabled again with the {@link Utils.Command#ENABLE ENABLE} command.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param command Command to disable
+        */
+       public static void disable(final Context context, final Address replyTo, final Command command){
+               PreferenceManager.getDefaultSharedPreferences(context).edit()
+               .putBoolean(command+"disabled", true).commit();
+               Utils.sendMessage(context, replyTo, command_disabled, command);
+       }
+
+       /**
+        * Re-enable a disabled Command.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param command Command to re-enable
+        */
+       public static void enable(final Context context, final Address replyTo, final Command command){
+               PreferenceManager.getDefaultSharedPreferences(context).edit()
+               .remove(command+"disabled").commit();
+               Utils.sendMessage(context, replyTo, command_enabled, command);
+
+       }
+
+       /**
+        * Check whether a Command is disabled.
+        *
+        * @param context Context instance
+        * @param command Command to check
+        * @return true if the Command is disabled, false otherwise
+        */
+       public static boolean isCommandDisabled(final Context context, final Command command){
+               return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(command+"disabled", false);
+       }
+
+       /**
+        * Poll the server for pending commands.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void poll(final Context context, final Address replyTo) {
+               Utils.sendMessage(context, replyTo, polling_server);
+               new PollServerAsyncTask().execute();
+       }
+
+       /**
+        * Change the server poll interval.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param ms server poll interval in milliseconds. If 0, server poll is disabled
+        */
+       public static void poll(final Context context, final Address replyTo, final long ms){
+               final AlarmManager man=(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+               final Intent pollAlarm=new Intent(context, LocalBroadcastReceiver.class);
+               pollAlarm.setAction(LocalBroadcastReceiver.ACTION_POLL_ALARM);
+               final PendingIntent intent=PendingIntent.getBroadcast(context, 0, pollAlarm, 0);
+               if(ms==0){
+                       Utils.unregisterOngoing(context, toNonNull(OngoingEvent.POLL));
+                       man.cancel(intent);
+                       Utils.sendMessage(context, replyTo, polling_stopped);
+               } else {
+                       Utils.registerOngoing(context, toNonNull(OngoingEvent.POLL));
+                       man.setRepeating(AlarmManager.RTC_WAKEUP, 0, ms, intent);
+                       Utils.sendMessage(context, replyTo, polling_every_milliseconds, Long.valueOf(ms));
+               }
+       }
+
+       /**
+        * Get an instance of {@link ITelephony}
+        *
+        * @param context Context instance
+        * @return an instance of {@link ITelephony}
+        * @throws NoSuchMethodException thrown by reflection
+        * @throws IllegalArgumentException thrown by reflection
+        * @throws IllegalAccessException thrown by reflection
+        * @throws InvocationTargetException thrown by reflection
+        */
+       private static ITelephony getITelephony(final Context context) throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException{
+               final TelephonyManager man=(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+               final Method m=TelephonyManager.class.getDeclaredMethod("getITelephony");
+               m.setAccessible(true);
+               return toNonNull((ITelephony) m.invoke(man));
+       }
+
+       /**
+        * Hang up the phone.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void hangup(final Context context, final Address replyTo){
+               try{
+                       getITelephony(context).endCall();
+               } catch(Exception e){
+                       Utils.sendMessage(context, replyTo, exception_while_hanging_up_call,
+                                       e.getClass().getName(), e.getMessage());
+               }
+       }
+
+       /**
+        * Answer the phone if it is ringing.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void answer(final Context context, final Address replyTo){
+               try{
+                       getITelephony(context).answerRingingCall();
+               } catch(Exception e){
+                       Utils.sendMessage(context, replyTo, exception_while_answering_call,
+                                       e.getClass().getName(), e.getMessage());
+               }
+       }
+
+       /**
+        * Launch a package.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param pkg name of the package to launch 
+        */
+       public static void launch(final Context context, final Address replyTo, final String pkg){
+               final Intent intent=context.getPackageManager().getLaunchIntentForPackage(pkg);
+               if(intent==null){
+                       Utils.sendMessage(context, replyTo, no_such_package);
+                       return;
+               }
+               context.startActivity(intent);
+               Utils.sendMessage(context, replyTo, app_launched);
+       }
+
+       /**
+        * Get the mobile data enabled status.
+        * 
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void data(final Context context, final Address replyTo){
+               try{
+                       final ConnectivityManager man=(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+                       final Method m=ConnectivityManager.class.getDeclaredMethod("getMobileDataEnabled");
+                       m.setAccessible(true);
+                       if(((Boolean)m.invoke(man)).booleanValue())
+                               Utils.sendMessage(context, replyTo, data_on);
+                       else
+                               Utils.sendMessage(context, replyTo, data_off);
+               } catch(Exception e){
+                       Utils.sendMessage(context, replyTo, exception_while_determining_data_state,
+                                       e.getClass().getName(), e.getMessage());
+               }
+       }
+
+       /**
+        * Set the mobile data enabled status.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param enable whether to enable mobile data
+        */
+       public static void data(final Context context, final Address replyTo, final boolean enable) {
+               try{
+                       if(enable){
+                               getITelephony(context).enableDataConnectivity();
+                               Utils.sendMessage(context, replyTo, enabling_data);
+                       } else {
+                               getITelephony(context).disableDataConnectivity();
+                               Utils.sendMessage(context, replyTo, disabling_data);
+                       }
+               } catch(Exception e){
+                       Utils.sendMessage(context, replyTo, exception_while_getting_itelephony,
+                                       e.getClass().getName(), e.getMessage());
+               }
+       }
+
+       /**
+        * Get the GPS status.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void gps(final Context context, final Address replyTo){
+               if(Secure.isLocationProviderEnabled(context.getContentResolver(), LocationManager.GPS_PROVIDER))
+                       Utils.sendMessage(context, replyTo, gps_on);
+               else
+                       Utils.sendMessage(context, replyTo, gps_off);
+       }
+
+       /**
+        * Set the GPS status.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param enabled requested GPS status
+        */
+       public static void gps(final Context context, final Address replyTo, final boolean enabled) {
+               Secure.setLocationProviderEnabled(context.getContentResolver(), LocationManager.GPS_PROVIDER, enabled);
+               if(enabled)
+                       Utils.sendMessage(context, replyTo, enabling_gps);
+               else
+                       Utils.sendMessage(context, replyTo, disabling_gps);
+       }
+
+       /**
+        * Get the Google location (aka network location) state.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        */
+       public static void glocation(final Context context, final Address replyTo){
+               if(Secure.isLocationProviderEnabled(context.getContentResolver(), LocationManager.NETWORK_PROVIDER))
+                       Utils.sendMessage(context, replyTo, network_location_on);
+               else
+                       Utils.sendMessage(context, replyTo, network_location_off);
+       }
+
+       /**
+        * Set the Google location (aka network location) state.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param enabled requested Google location state
+        */
+       public static void glocation(final Context context, final Address replyTo, final boolean enabled) {
+               Secure.setLocationProviderEnabled(context.getContentResolver(), LocationManager.NETWORK_PROVIDER, enabled);
+               if(enabled)
+                       Utils.sendMessage(context, replyTo, enabling_network_location);
+               else
+                       Utils.sendMessage(context, replyTo, disabling_network_location);
+       }
+
+       /**
+        * Reboot the phone.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param reason reboot reason
+        *
+        * @see PowerManager#reboot(String)
+        */
+       public static void reboot(final Context context, final Address replyTo, final @Nullable String reason) {
+               final PowerManager pm=(PowerManager) context.getSystemService(Context.POWER_SERVICE);
+               Utils.sendMessage(context, replyTo, rebooting); 
+               pm.reboot(reason);
+       }
+
+       /**
+        * Cancel a notification.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param id notification ID 
+        */
+       public static void notify(final Context context, final Address replyTo, final int id) {
+               final NotificationManager man=(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+               man.cancel(id);
+               Utils.sendMessage(context, replyTo, notification_canceled);
+       }
+
+       /**
+        * Show a notification.
+        *
+        * @param context Context instance
+        * @param replyTo reply Address
+        * @param id notification ID
+        * @param title notificationO title
+        * @param text notification text
+        */
+       public static void notify(final Context context, final Address replyTo, final int id, final String title, final String text) {
+               final NotificationManager man=(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+               man.notify(id, new NotificationCompat.Builder(context).
+                               setContentTitle(title).
+                               setContentText(text).
+                               setSmallIcon(android.R.drawable.stat_notify_sync_noanim).
+                               build());
+               Utils.sendMessage(context, replyTo, notification_shown);
+       }
+}
diff --git a/src/ro/ieval/fonbot/LocalBroadcastReceiver.java b/src/ro/ieval/fonbot/LocalBroadcastReceiver.java
new file mode 100644 (file)
index 0000000..6605e3d
--- /dev/null
@@ -0,0 +1,49 @@
+package ro.ieval.fonbot;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * BroadcastReceiver that receives appwide local broadcasts.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class LocalBroadcastReceiver extends BroadcastReceiver {
+       /**
+        * Broadcast action: the poll alarm has fired, the app should poll the server
+        */
+       public static final String ACTION_POLL_ALARM="ro.ieval.fonbot.SystemEventReceiver.ACTION_POLL_ALARM";
+
+       @Override
+       public void onReceive(final @Nullable Context context, final @Nullable Intent intent) {
+               if(intent==null)
+                       return;
+
+               final String action=intent.getAction();
+               if(action.equals(LocalBroadcastReceiver.ACTION_POLL_ALARM))
+                       new PollServerAsyncTask().execute();
+       }
+
+}
diff --git a/src/ro/ieval/fonbot/PollServerAsyncTask.java b/src/ro/ieval/fonbot/PollServerAsyncTask.java
new file mode 100644 (file)
index 0000000..7e95303
--- /dev/null
@@ -0,0 +1,133 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import ro.ieval.fonbot.Address.Protocol;
+import android.os.AsyncTask;
+import android.util.Log;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * AsyncTask that polls the server for pending commands and executes any commands found.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class PollServerAsyncTask extends AsyncTask<Void, Void, List<PollServerAsyncTask.Command>> {
+       /**
+        * POJO that represents a command in a basic form: a command name, a list of arguments and a reply address, all of them being strings
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       final class Command{
+               /**
+                * The command name
+                */
+               final String command;//NOPMD no confusion here
+               /**
+                * The list of command arguments
+                */
+               final String[] args;
+               /**
+                * The reply address, excluding the protocol
+                */
+               final String replyto;
+
+               /**
+                * Construct a Command from its three parts
+                *
+                * @param command the command name
+                * @param args the command arguments
+                * @param replyto the reply address
+                */
+               Command(final String command, final String[] args, final String replyto) {//NOPMD array is immutable
+                       this.command=command;
+                       this.args=args;
+                       this.replyto=replyto;
+               }
+       }
+
+       @Override
+       protected List<Command> doInBackground(final @Nullable Void... params) {
+               Log.d(getClass().getName(), "Polling server");
+               final List<Command> commands=new ArrayList<Command>(10);
+               try {
+                       final URL url=new URL("http://ieval.ro:7777/get");
+                       final HttpURLConnection conn=(HttpURLConnection) url.openConnection();
+                       conn.setRequestProperty("X-ID", FonBotApplication.instance.regID);
+                       conn.connect();
+                       final byte[] buf=new byte[4096*1024];
+                       Log.d(getClass().getName(), "Server poll got response code "+conn.getResponseCode()+" and message "+conn.getResponseMessage());
+                       if(conn.getResponseCode()!=200)
+                               return commands;
+                       conn.getInputStream().read(buf);
+                       final JSONArray array=new JSONArray(new String(buf));
+                       final List<String> list=new ArrayList<String>();
+                       for(int i=0;i<array.length();i++){
+                               final JSONObject object=array.getJSONObject(i);
+
+                               for(int j=0;object.has("arg"+j);j++)
+                                       list.add(object.getString("arg"+j));
+
+                               commands.add(new Command(//NOPMD command changes for each JSON object
+                                               toNonNull(object.getString("command")),
+                                               toNonNull(list.toArray(new String[list.size()])),
+                                               toNonNull(object.getString("replyto"))));
+                               list.clear();
+                       }
+               } catch (MalformedURLException e) {
+                       e.printStackTrace();
+               } catch (IOException e) {
+                       e.printStackTrace();
+               } catch (JSONException e) {
+                       e.printStackTrace();
+               }
+               if(commands==null)
+                       throw new AssertionError("commands is null in PollServerAsyncTask#doInBackground");//silence Eclipse bug
+               return commands;
+       }
+
+       @Override
+       protected void onPostExecute(final @Nullable List<Command> commands) {
+               if(commands==null)
+                       return;
+               for (Command command : commands) {
+                       Log.d(getClass().getName(), "Poll got command "+command.command+" with "+((command.args.length==0)?"no args":"args "+Utils.join(
+                                       " ",toNonNull(command.args))));
+                       Utils.processCommand(
+                                       toNonNull(FonBotApplication.instance),
+                                       toNonNull(command.command),
+                                       toNonNull(command.args),
+                                       new Address(toNonNull(Protocol.HTTP), command.replyto));//NOPMD address depends on command
+               }
+       }
+}
diff --git a/src/ro/ieval/fonbot/ProtectedBroadcastReceiver.java b/src/ro/ieval/fonbot/ProtectedBroadcastReceiver.java
new file mode 100644 (file)
index 0000000..6cfe312
--- /dev/null
@@ -0,0 +1,57 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.Utils.MessageType;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * BroadcastReceiver that receives various broadcasts sent by the sistem
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class ProtectedBroadcastReceiver extends BroadcastReceiver {
+       @Override
+       public void onReceive(@Nullable final Context context, @Nullable final Intent intent) {
+               if(context==null || intent==null)
+                       return;
+
+               final String action=intent.getAction();
+
+               if(action.equals(Intent.ACTION_BATTERY_LOW)){
+                       Utils.sendMessage(context, toNonNull(MessageType.BATTERY),
+                                       toNonNull(context.getString(battery_low)));
+                       Heavy.describeBatteryLevel(context, null, toNonNull(MessageType.BATTERY));
+               } else if(action.equals(Intent.ACTION_BATTERY_OKAY)){
+                       Utils.sendMessage(context, toNonNull(MessageType.BATTERY),
+                                       toNonNull(context.getString(battery_okay)));
+                       Heavy.describeBatteryLevel(context, null, toNonNull(MessageType.BATTERY));
+               }
+       }
+
+}
diff --git a/src/ro/ieval/fonbot/RemoteCrashdumpHandler.java b/src/ro/ieval/fonbot/RemoteCrashdumpHandler.java
new file mode 100644 (file)
index 0000000..4f28d84
--- /dev/null
@@ -0,0 +1,73 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.content.Context;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * UncaughtExceptionHandler that sends exceptions to the server.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+final class RemoteCrashdumpHandler implements UncaughtExceptionHandler {
+       /** Context instance */
+       private final Context context;
+
+       /**
+        * Constructs a RemoteCrashdumpHandler with a given Context instance.
+        * 
+        * @param context Context instance
+        */
+       public RemoteCrashdumpHandler(final Context context) {
+               this.context=context;
+       }
+
+       @Override
+       public void uncaughtException(final @Nullable Thread thread, final @Nullable Throwable ex) {
+               if(ex==null)
+                       return;
+
+               final ByteArrayOutputStream baos=new ByteArrayOutputStream(16384);
+               final PrintStream pw=new PrintStream(baos);
+               ex.printStackTrace(pw);
+               pw.close();
+
+               new SendHttpMessageAsyncTask(toNonNull(Arrays.asList(
+                               new Header("X-Action", "CRASHDUMP")
+                               )), toNonNull(context), toNonNull(baos.toByteArray())).execute();
+
+               try {
+                       baos.close();
+               } catch (IOException e) {//NOPMD close exceptions are unimportant
+                       //ignored
+               }
+       }
+
+}
diff --git a/src/ro/ieval/fonbot/SendHttpMessageAsyncTask.java b/src/ro/ieval/fonbot/SendHttpMessageAsyncTask.java
new file mode 100644 (file)
index 0000000..a40f7ed
--- /dev/null
@@ -0,0 +1,163 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.util.Log;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * AsyncTask that sends a HTTP request to the server and broadcasts back the response message & response code
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class SendHttpMessageAsyncTask extends AsyncTask<String, Void, String> {
+       /**
+        * Extra: the response message 
+        */
+       public static final String EXTRA_RESPONSE_MESSAGE="response_message";
+       /**
+        * Extra: the response code
+        */
+       public static final String EXTRA_RESPONSE_CODE="response_code";
+
+       /**
+        * List of extra request headers.
+        */
+       private final Collection<Header> headers;
+       /**
+        * Action to send back in the response broadcast
+        */
+       private final String broadcast;
+       /**
+        * Context instance used by this class
+        */
+       private final Context context;
+       /**
+        * Data to send to the server
+        */
+       private final byte[] data;
+       /**
+        * Response code from the server
+        */
+       private int responseCode;
+
+       /**
+        * Constructs a SendHttpMessageAsyncTask which sends a string-based message and does not broadcast the response. 
+        *
+        * @param headers the extra headers
+        * @param context the context instance
+        */
+       public SendHttpMessageAsyncTask(final Collection<Header> headers, final Context context){
+               super();
+               this.headers=headers;
+               this.broadcast=null;//NOPMD final field
+               this.context=context;
+               this.data=null;//NOPMD final field
+       }
+
+       /**
+        * Constructs a SendHttpMessageAsyncTask which sends a binary message and does not broadcast the response.
+        *
+        * @param headers the extra headers
+        * @param context the context instance
+        * @param data the message to send
+        */
+       public SendHttpMessageAsyncTask(final Collection<Header> headers, final Context context, final byte[] data){//NOPMD array is supposed to be immutable.
+               super();
+               this.headers=headers;
+               this.broadcast=null;//NOPMD final field
+               this.context=context;
+               this.data=data;
+       }
+
+       /**
+        * Constructs a SendHttpMessageAsyncTask which sends a string-based message and broadcasts the response.
+        *
+        * @param headers the extra headers
+        * @param broadcast the broadcast to send
+        * @param context the context instance
+        */
+       public SendHttpMessageAsyncTask(final Collection<Header> headers, final String broadcast, final Context context){
+               super();
+               this.headers=headers;
+               this.broadcast=broadcast;
+               this.context=context;
+               this.data=null;//NOPMD final field
+       }
+
+       @Override
+       protected String doInBackground(final @Nullable String... args) {
+               if(args==null)
+                       return "";
+
+               final byte[] msg;
+               if(data==null)
+                       msg=Utils.join(" ", args).getBytes();
+               else
+                       msg=data;
+
+               try {
+                       final URL url=new URL("http://ieval.ro:7777/");
+                       final HttpURLConnection conn=(HttpURLConnection) url.openConnection();
+                       conn.setDoOutput(true);
+                       conn.setFixedLengthStreamingMode(msg.length);
+                       conn.setRequestProperty("X-ID", FonBotApplication.instance.regID);
+                       for (Header header : headers)
+                               conn.setRequestProperty(header.name, header.value);
+                       conn.connect();
+                       final OutputStream stream=conn.getOutputStream();
+                       stream.write(msg);
+                       stream.close();
+                       Log.d(getClass().getName(),"HTTP Response: "+conn.getResponseCode()+" "+conn.getResponseMessage());
+                       conn.disconnect();
+                       final String message=conn.getResponseMessage();
+                       responseCode=conn.getResponseCode();
+                       if(message==null)
+                               return toNonNull(context.getString(no_response_returned_from_server));
+                       return Utils.parseHttpMessage(message);
+               } catch (Exception e) {
+                       return toNonNull(context.getString(connection_error));
+               }
+       }
+
+       @Override
+       protected void onPostExecute(final @Nullable String result) {
+               super.onPostExecute(result);
+               if(broadcast!=null){
+                       final Intent intent=new Intent(broadcast);
+                       intent.putExtra(EXTRA_RESPONSE_MESSAGE, result);
+                       intent.putExtra(EXTRA_RESPONSE_CODE, responseCode);
+                       context.sendBroadcast(intent);
+               }
+       }
+}
diff --git a/src/ro/ieval/fonbot/SmsReceiver.java b/src/ro/ieval/fonbot/SmsReceiver.java
new file mode 100644 (file)
index 0000000..49d4bea
--- /dev/null
@@ -0,0 +1,86 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.*;
+
+import static ro.ieval.fonbot.Utils.toNonNull;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.Address.Protocol;
+import ro.ieval.fonbot.Utils.MessageType;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.preference.PreferenceManager;
+import android.telephony.SmsMessage;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * BroadcastReceiver that receives SMSes. It processes command smses and sends {@link MessageType#SMS} notifications.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class SmsReceiver extends BroadcastReceiver {
+
+       @Override
+       public void onReceive(@Nullable final Context context, @Nullable final Intent intent) {
+               if(intent==null||context==null)
+                       return;
+
+               final Object[] pdus=(Object[]) intent.getExtras().get("pdus");
+               for (Object pdu : pdus) {
+                       final SmsMessage sms=SmsMessage.createFromPdu((byte[]) pdu);
+                       final String originAddress=sms.getOriginatingAddress();
+                       if(sms.getMessageBody() == null || originAddress == null)
+                               return;
+
+                       final String name=Utils.callerId(context, originAddress);
+                       if(name==null)
+                               Utils.sendMessage(context, toNonNull(MessageType.SMS),
+                                               toNonNull(context.getString(sms_received_fmt,
+                                                               originAddress, sms.getMessageBody())));
+                       else
+                               Utils.sendMessage(context, toNonNull(MessageType.SMS),
+                                               toNonNull(context.getString(sms_received_fmt,
+                                                               originAddress+" ("+name+")",sms.getMessageBody())));
+
+                       final String[] lines=sms.getMessageBody().split("\n");
+                       final String password = PreferenceManager.getDefaultSharedPreferences(context).getString("smspassword","");
+                       if(password==null||password.length()==0)
+                               return;
+
+                       if(lines.length==0 || !lines[0].equals(password))
+                               return;
+
+                       final Address address=new Address(toNonNull(Protocol.SMS), originAddress);//NOPMD variable depends on originAddress
+                       for (int i = 1; i < lines.length; i++) {
+                               final String[] words=Utils.shellwords(toNonNull(lines[i]));
+                               final String[] args=new String[words.length-1];//NOPMD variable size depends on words.length
+                               System.arraycopy(words, 1, args, 0, args.length);
+
+                               Utils.processCommand(context, toNonNull(words[0]), args, toNonNull(address));
+                       }
+
+                       abortBroadcast();
+               }
+       }
+
+}
diff --git a/src/ro/ieval/fonbot/SmsStatusReceiver.java b/src/ro/ieval/fonbot/SmsStatusReceiver.java
new file mode 100644 (file)
index 0000000..30809ae
--- /dev/null
@@ -0,0 +1,105 @@
+package ro.ieval.fonbot;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telephony.SmsManager;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * BroadcastReceiver that receives SMS status broadcasts, notifying the user.
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class SmsStatusReceiver extends BroadcastReceiver {
+       /**
+        * Formats a SMS message intent into a human-readable string
+        *
+        * @param intent the SMS message
+        * @return the string representation of the SMS
+        */
+       private static String describeSMS(final Intent intent){
+               return " ("+"To: "+intent.getStringExtra(EXTRA_DESTINATION)+
+                               ", Part: "+intent.getIntExtra(EXTRA_PART, 1)+" of "+intent.getIntExtra(EXTRA_TOTAL, 1)+")";
+       }
+
+       /**
+        * Broadcast action: SMS sent
+        */
+       public static final String SENT_ACTION="ro.ieval.fonbot.SmsStatusReceiver.SENT";
+       /**
+        * Broadcast action: SMS delivered
+        */
+       public static final String DELIVERED_ACTION="ro.ieval.fonbot.SmsStatusReceiver.DELIVERED";
+       /**
+        * Extra: SMS destination (phone number)
+        */
+       public static final String EXTRA_DESTINATION="destination";
+       /**
+        * Extra: SMS part number
+        */
+       public static final String EXTRA_PART="part";
+       /**
+        * Extra: SMS part count
+        */
+       public static final String EXTRA_TOTAL="total";
+       /**
+        * Extra: Notification address
+        */
+       public static final String EXTRA_REPLY_TO="reply_to";
+
+       @Override
+       public void onReceive(@Nullable final Context context, @Nullable final Intent intent) {
+               if(intent==null||context==null)
+                       return;
+               final Address replyTo=new Address(Utils.toNonNull(intent.getStringExtra(EXTRA_REPLY_TO)));
+               if(intent.getAction().startsWith(SENT_ACTION))
+                       switch(getResultCode()){
+                       case Activity.RESULT_OK:
+                               Utils.sendMessage(context, replyTo, "SMS sent"+describeSMS(intent));
+                               break;
+                       case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+                               Utils.sendMessage(context, replyTo, "SMS not sent: Generic failure"+describeSMS(intent));
+                               break;
+                       case SmsManager.RESULT_ERROR_NO_SERVICE:
+                               Utils.sendMessage(context, replyTo, "SMS not sent: No service"+describeSMS(intent));
+                               break;
+                       case SmsManager.RESULT_ERROR_NULL_PDU:
+                               Utils.sendMessage(context, replyTo, "SMS not sent: Null PDU"+describeSMS(intent));
+                               break;
+                       case SmsManager.RESULT_ERROR_RADIO_OFF:
+                               Utils.sendMessage(context, replyTo, "SMS not sent: Radio off"+describeSMS(intent));
+                               break;
+                       }
+               else if(intent.getAction().startsWith(DELIVERED_ACTION))
+                       switch(getResultCode()){
+                       case Activity.RESULT_OK:
+                               Utils.sendMessage(context, replyTo, "SMS delivered"+describeSMS(intent));
+                               break;
+                       case Activity.RESULT_CANCELED:
+                               Utils.sendMessage(context, replyTo, "SMS not delivered"+describeSMS(intent));
+                               break;
+                       }
+       }
+}
diff --git a/src/ro/ieval/fonbot/Utils.java b/src/ro/ieval/fonbot/Utils.java
new file mode 100644 (file)
index 0000000..d5e0f5b
--- /dev/null
@@ -0,0 +1,1033 @@
+package ro.ieval.fonbot;
+
+import static ro.ieval.fonbot.R.string.cannot_parse_count;
+import static ro.ieval.fonbot.R.string.cannot_parse_interval;
+import static ro.ieval.fonbot.R.string.cannot_parse_min_distance;
+import static ro.ieval.fonbot.R.string.cannot_parse_min_time;
+import static ro.ieval.fonbot.R.string.cannot_parse_port;
+import static ro.ieval.fonbot.R.string.cannot_parse_provider_allowed_values_are;
+import static ro.ieval.fonbot.R.string.command_disabled;
+import static ro.ieval.fonbot.R.string.could_not_parse_argument_allowed_values_are;
+import static ro.ieval.fonbot.R.string.could_not_parse_ms;
+import static ro.ieval.fonbot.R.string.error_while_processing_command;
+import static ro.ieval.fonbot.R.string.invalid_length_allowed_values_are;
+import static ro.ieval.fonbot.R.string.invalid_ringer_mode_valid_values_are;
+import static ro.ieval.fonbot.R.string.location_tracking_is_active;
+import static ro.ieval.fonbot.R.string.messagetype_should_be_one_of;
+import static ro.ieval.fonbot.R.string.no_such_command_command_list;
+import static ro.ieval.fonbot.R.string.notification_disabled;
+import static ro.ieval.fonbot.R.string.notification_enabled;
+import static ro.ieval.fonbot.R.string.ringing;
+import static ro.ieval.fonbot.R.string.security_exception;
+import static ro.ieval.fonbot.R.string.the_polling_service_is_running;
+import static ro.ieval.fonbot.R.string.the_second_argument_to_wipe_must_be;
+import static ro.ieval.fonbot.R.string.unknown_command;
+import static ro.ieval.fonbot.R.string.wipetype_should_be_one_of;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+import ro.ieval.fonbot.Address.Protocol;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.location.LocationManager;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telephony.SmsManager;
+import android.util.Log;
+import android.widget.Toast;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
+
+/**
+ * Utility functions and enums used in various places. 
+ *
+ * @author Marius Gavrilescu <marius@ieval.ro>
+ */
+public final class Utils {
+       /**
+        * Enum of all FonBot commands.
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       @SuppressWarnings("javadoc")
+       public static enum Command{
+               TOAST, ECHO, SMS, FLASH, WIFI,
+               BLUETOOTH, DIAL, RING, SPEAK, VIBRATE,
+               DIALOG, LOCATION, NOLOCATION, RINGER, NCFILE,
+               PHOTO, SETNOTIFICATION, DELNOTIFICATION, SETPASSWORD, HELP,
+               WIPE, LOCK, VIEW, PLAY, PAUSE,
+               NEXT, PREV, BATT, CALLLOG, SMSLOG,
+               LS, RM, CONTACTS, DISABLE, ENABLE,
+               POLL, HANGUP, ANSWER, LAUNCH, DATA,
+               GPS, GLOCATION, REBOOT, SHUTDOWN, NOTIFY
+       }
+
+       /**
+        * Enum of all message types. Each message type is a kind of notification which can be enabled/disabled by the user and directed to a certain address.
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        *
+        */
+       public static enum MessageType{
+               /** SMS notifications */
+               SMS, 
+               /** Phone state (idle, offhook or ringing) notifications */
+               PHONE_STATE,
+               /** Lockscreen failed password notifications */
+               WATCH_LOGIN,
+               /** Device admin enable/disable notifications */
+               ADMIN,
+               /** Coarse battery status (low battery, ok battery) notifications */
+               BATTERY,
+               /** Fine battery status notifications */
+               BATTERY_CHANGED,
+               /** Headset plug/unplug notifications */
+               HEADSET
+       }
+
+       /**
+        * Enum of wipe types. The two defined types are the data wipe (which does not include the SD card) and the full wipe (which includes the SD card).
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       public static enum WipeType{
+               /** Factory reset the phone, without touching the SD card */
+               DATA,
+               /** Factory reset the phone and wipe the SD card too */
+               FULL
+       }
+
+       /**
+        * Enum of ringer modes.
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       public static enum RingerMode{
+               /** Sound is on */
+               NORMAL,
+               /** Sound is off, vibrate is on */
+               VIBRATE,
+               /** Sound is off, vibrate is off */
+               SILENT
+       }
+
+       /**
+        * Enum of location providers
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       public static enum LocationProvider{
+               /** The network location provider */
+               NETWORK,
+               /** The GPS location provider */
+               GPS
+       }
+
+       /**
+        * Enum of toast lengths.
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       private static enum ToastLength{
+               /** Long toast */
+               LONG,
+               /** Short toast */
+               SHORT
+       }
+
+       /**
+        * Enum of the values on and off. Used for boolean arguments.
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       @SuppressWarnings("javadoc")
+       private static enum OnOff{
+               ON, OFF
+       }
+
+       /**
+        * Enum of ongoing event types
+        *
+        * @author Marius Gavrilescu <marius@ieval.ro>
+        */
+       public static enum OngoingEvent{
+               /** Location tracking is active. Registered by {@link Command#LOCATION}, unregistered by {@link Command#NOLOCATION} */
+               LOCATION(location_tracking_is_active),
+               /** The phone is ringing. Registered/unregistered by {@link Command#RING} */
+               RING(ringing),
+               /** The polling alarm is on. Registered/unregistered by {@link Command#POLL} */
+               POLL(the_polling_service_is_running);
+
+               /** String resource: the event description */
+               public final int resource;
+
+               /**
+                * Constructs an OngoingEvent from its event description.
+                *
+                * @param resource the event description
+                */
+               private OngoingEvent(final int resource) {
+                       this.resource=resource;
+               }
+       }
+
+       /** Confirmation string for the {@link Command#WIPE WIPE} command. */
+       public static final String WIPE_CONFIRM_STRING="I am aware this cannot be undone";
+
+       /**
+        * Converts a Nullable object into a NonNull one.
+        *
+        * @param object the Nullable object to convert
+        * @return a NonNull object equivalent to the Nullable parameter
+        * @throws AssertionError if the given object is null
+        */
+       public static <T> T toNonNull(@Nullable T object) throws AssertionError{
+               if(object==null){
+                       Log.wtf(Utils.class.getName(), "toNonNull called with null");
+                       throw new AssertionError("Log.wtf did not terminate the process");
+               }
+               return object;
+       }
+
+       /**
+        * Join an array of Objects with elements separated by a separator into a single string.
+        *
+        * @param separator the separator
+        * @param offset the offset into the array
+        * @param args the array of Objects to join
+        * @return the joined string
+        */
+       public static String join(final String separator,final int offset,final Object[] args){
+               final StringBuilder sb=new StringBuilder(240);
+               sb.append(args[offset]);
+               for (int i = offset+1; i < args.length; i++) {
+                       sb.append(separator);
+                       sb.append(args[i]);
+               }
+               return toNonNull(sb.toString());
+       }
+
+       /**
+        * Join an array of Objects with elements separated by a separator into a single string.
+        *
+        * @param separator the separator
+        * @param args the array of objects to join
+        * @return the joined string
+        */
+       public static String join(final String separator, final Object[] args){
+               return join(separator,0,args);
+       }
+
+       /**
+        * Send a notification to the user.
+        *
+        * @param context Context instance
+        * @param type notification type
+        * @param resource String resource for the message text
+        */
+       public static void sendMessage(final Context context, final MessageType type, final int resource){
+               sendMessage(context, type, toNonNull(context.getString(resource)));
+       }
+
+       /**
+        * Send a notification to the user.
+        *
+        * @param context Context instance
+        * @param type notification type
+        * @param resource String resource for the message text
+        * @param args format parameters for the resource
+        */
+       public static void sendMessage(final Context context, final MessageType type, final int resource, final Object... args){
+               sendMessage(context, type, toNonNull(context.getString(resource, args)));
+       }
+
+       /**
+        * Send a message to a certain Address.
+        *
+        * @param context Context instance
+        * @param address destination Address
+        * @param resource String resource for the message text
+        */
+       public static void sendMessage(final Context context, final Address address, final int resource){
+               sendMessage(context, address, toNonNull(context.getString(resource)));
+       }
+
+       /**
+        * Send a message to a certain Address.
+        *
+        * @param context Context instance
+        * @param address destination Address
+        * @param resource String resource for the message text
+        * @param args format parameters for the resource
+        */
+       public static void sendMessage(final Context context, final Address address, final int resource, final Object... args){
+               sendMessage(context, address, toNonNull(context.getString(resource, args)));
+       }
+
+       /**
+        * Send a notification to the user.
+        * 
+        * @param context Context instance
+        * @param type notification type
+        * @param msg the notification text
+        */
+       public static void sendMessage(final Context context, final MessageType type,final String msg){
+               final SharedPreferences sp=PreferenceManager.getDefaultSharedPreferences(context);
+               final String address=sp.getString(type.toString(),null);
+               if(address==null)
+                       return;
+               sendMessage(context, new Address(address), msg);
+       }
+
+       /**
+        * Send a message to a certain Address.
+        *
+        * @param context Context instance
+        * @param address destination Address
+        * @param message the message text
+        */
+       public static void sendMessage(final Context context, final Address address, final String message){
+               switch(address.protocol){
+               case HTTP:
+                       new SendHttpMessageAsyncTask(toNonNull(Arrays.asList(
+                                       new Header("X-Action", "SEND "+address.data))), context).execute(message);
+                       break;
+
+               case SMS:
+                       SmsManager.getDefault().sendTextMessage(address.data, null, message, null, null);
+                       break;
+
+               case LOCAL:
+                       final Intent intent = new Intent(FonBotLocalActivity.RESPONSE_RECEIVED_ACTION);         
+                       intent.putExtra(FonBotLocalActivity.EXTRA_RESPONSE, message);
+                       context.sendBroadcast(intent);
+                       break;
+
+               case NULL:
+                       break;//blackhole
+               }
+       }
+
+       /**
+        * Parses a HTTP response message of the form <code>(part1) part2</code>, returning <code>part2</code>.
+        *
+        * @param message the raw HTTP response message
+        * @return the extracted part
+        */
+       public static String parseHttpMessage(final String message){
+               final int indexOfParen=message.indexOf(')');
+               if(indexOfParen==-1)
+                       return message;
+               return toNonNull(message.substring(indexOfParen+2));
+       }
+
+       /**
+        * Splits a string into words.
+        *
+        * @param string the string
+        * @return an array of words
+        */
+       public static String[] shellwords(final String string){
+               return toNonNull(string.split("[ ]+(?=([^\"]*\"[^\"]*\")*[^\"]*$)"));//TODO: Make this handle backslash escapes
+       }
+
+       /**
+        * Returns the name associated with a phone number.
+        *
+        * @param context Context instance
+        * @param number phone number to search for
+        * @return the name associated with this number, or null if the number was not found in the contacts.
+        */
+       public static @Nullable String callerId(final Context context, final String number){
+               final Cursor cursor=context.getContentResolver().query(
+                               Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+                               new String[]{PhoneLookup.DISPLAY_NAME},
+                               null,
+                               null,
+                               null);
+
+               if(cursor.moveToFirst()){
+                       final String name=cursor.getString(0);
+                       cursor.close();
+                       return name;
+               }
+               cursor.close();
+               return null;
+       }
+
+       /**
+        * Registers an ongoing event.
+        *
+        * @param context Context instance
+        * @param event event to register
+        */
+       public static void registerOngoing(final Context context, final OngoingEvent event){
+               final Intent intent=new Intent(context, FonBotMainService.class);
+               intent.setAction(FonBotMainService.ACTION_PUT_ONGOING);
+               intent.putExtra(FonBotMainService.EXTRA_ONGOING_ID, event.ordinal());
+               context.startService(intent);
+       }
+
+       /**
+        * Unregisters an ongoing event
+        *
+        * @param context Context instance
+        * @param event event to unregister
+        */
+       public static void unregisterOngoing(final Context context, final OngoingEvent event){
+               final Intent intent=new Intent(context, FonBotMainService.class);
+               intent.setAction(FonBotMainService.ACTION_DELETE_ONGOING);
+               intent.putExtra(FonBotMainService.EXTRA_ONGOING_ID, event.ordinal());
+               context.startService(intent);
+       }
+
+       /**
+        * Executes a given command
+        *
+        * @param context Context instance
+        * @param cmd Command to execute
+        * @param args arguments for the command
+        * @param replyTo Address to send replies to
+        */
+       private static void processCommand(final Context context, final Command cmd,final String[] args,final Address replyTo){
+               if(Heavy.isCommandDisabled(context, cmd)){
+                       sendMessage(context, replyTo, command_disabled);
+                       return;
+               }
+
+               switch(cmd){
+               case TOAST:
+                       if(args.length < 1 || args.length > 2){
+                               Heavy.help(context, replyTo, toNonNull(Command.TOAST));
+                               break;
+                       }
+
+                       if(args.length==1)
+                               Heavy.toast(context, replyTo, toNonNull(args[0]));
+                       else {
+                               try {
+                                       switch(ToastLength.valueOf(args[1].toUpperCase(Locale.ENGLISH))){
+                                       case LONG:
+                                               Heavy.toast(context, replyTo, toNonNull(args[0]), Toast.LENGTH_LONG);
+                                               break;
+                                       case SHORT:
+                                               Heavy.toast(context, replyTo, toNonNull(args[0]), Toast.LENGTH_SHORT);
+                                       }
+                               } catch(IllegalArgumentException e){
+                                       sendMessage(context, replyTo, invalid_length_allowed_values_are, join(", ",toNonNull(ToastLength.values())));
+                               }
+                       }
+                       break;
+
+               case ECHO:
+                       if(args.length==0){
+                               Heavy.help(context, replyTo, toNonNull(Command.ECHO));
+                               break;
+                       }
+                       sendMessage(context, replyTo, join(" ",args));
+                       break;
+
+               case SMS:
+                       if(args.length < 2){
+                               Heavy.help(context, replyTo, toNonNull(Command.SMS));
+                               break;
+                       }
+                       Heavy.sms(context, replyTo, toNonNull(args[0]), join(" ", 1, args));
+                       break;
+
+               case FLASH:
+                       if(args.length != 1){
+                               Heavy.help(context, replyTo, toNonNull(Command.FLASH));
+                               break;
+                       }
+
+                       try {
+                               Heavy.flash(context, replyTo, OnOff.valueOf(args[0].toUpperCase(Locale.ENGLISH)) == OnOff.ON);
+                       } catch(IllegalArgumentException e) {
+                               sendMessage(context, replyTo, could_not_parse_argument_allowed_values_are, join(", ", toNonNull(OnOff.values())));
+                       }
+                       break;
+
+               case WIFI:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.WIFI));
+                               break;
+                       }
+                       if(args.length==0)
+                               Heavy.wifi(context, replyTo);
+                       else {
+                               try {
+                                       Heavy.wifi(context, replyTo, OnOff.valueOf(args[0].toUpperCase(Locale.ENGLISH)) == OnOff.ON);
+                               } catch(IllegalArgumentException e) {
+                                       sendMessage(context, replyTo, could_not_parse_argument_allowed_values_are, join(", ", toNonNull(OnOff.values())));
+                               }
+                       }
+                       break;
+
+               case BLUETOOTH:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.BLUETOOTH));
+                               break;
+                       }
+                       if(args.length==0)
+                               Heavy.bluetooth(context, replyTo);
+                       else {
+                               try {
+                                       Heavy.bluetooth(context, replyTo, OnOff.valueOf(args[0].toUpperCase(Locale.ENGLISH)) == OnOff.ON);
+                               } catch(IllegalArgumentException e) {
+                                       sendMessage(context, replyTo, could_not_parse_argument_allowed_values_are, join(", ", toNonNull(OnOff.values())));
+                               }
+                       }
+                       break;
+
+               case DIAL:
+                       if(args.length!=1){
+                               Heavy.help(context, replyTo, toNonNull(Command.DIAL));
+                               break;
+                       }
+                       Heavy.dial(context, replyTo, toNonNull(args[0]));
+                       break;
+
+               case RING:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.RING));
+                               break;
+                       }
+                       if(args.length==0)
+                               Heavy.ring(context, replyTo);
+                       else {
+                               try {
+                                       Heavy.ring(context, replyTo, OnOff.valueOf(args[0].toUpperCase(Locale.ENGLISH)) == OnOff.ON);
+                               } catch(IllegalArgumentException e){
+                                       sendMessage(context, replyTo, could_not_parse_argument_allowed_values_are, join(", ", toNonNull(OnOff.values())));
+                               }
+                       }
+                       break;
+
+               case SPEAK:
+                       if(args.length==0){
+                               Heavy.help(context, replyTo, toNonNull(Command.SPEAK));
+                               break;
+                       }
+                       Heavy.speak(context, replyTo, join(" ",args));
+                       break;
+
+               case VIBRATE:
+                       if(args.length!=1){
+                               Heavy.help(context, replyTo, toNonNull(Command.VIBRATE));
+                               break;
+                       }
+                       final long ms;
+                       try{
+                               ms=Long.parseLong(args[0]);
+                       } catch(NumberFormatException e){
+                               sendMessage(context, replyTo, could_not_parse_ms);
+                               break;
+                       }
+                       Heavy.vibrate(context, replyTo, ms);
+                       break;
+
+               case DIALOG://TODO: Should add an edittext
+                       if(args.length<1){
+                               Heavy.help(context, replyTo, toNonNull(Command.DIALOG));
+                               break;
+                       }
+                       final String[] buttons=new String[args.length-1];
+                       System.arraycopy(args,1,buttons,0,buttons.length);
+                       Heavy.dialog(context, replyTo, toNonNull(args[0]), buttons);
+                       break;
+
+               case LOCATION:
+                       if(args.length>3||args.length<1){
+                               Heavy.help(context, replyTo, toNonNull(Command.LOCATION));
+                               break;
+                       }
+
+                       final @NonNull String provider;
+                       try{
+                               switch(LocationProvider.valueOf(args[0].toUpperCase(Locale.ENGLISH))){
+                               case GPS:
+                                       provider=toNonNull(LocationManager.GPS_PROVIDER);
+                                       break;
+                               case NETWORK:
+                               default:
+                                       provider=toNonNull(LocationManager.NETWORK_PROVIDER);
+                                       break;
+                               }
+                       } catch(IllegalArgumentException e){
+                               sendMessage(context, replyTo, cannot_parse_provider_allowed_values_are, join(", ",toNonNull(LocationProvider.values())));
+                               break;
+                       }
+
+                       final long minTime;
+                       final float minDistance;
+
+                       if(args.length>1)
+                               try{
+                                       minTime=Long.parseLong(args[1]);
+                               } catch (NumberFormatException e){
+                                       sendMessage(context, replyTo, cannot_parse_min_time);
+                                       break;
+                               }
+                       else
+                               minTime=500;
+
+                       if(args.length>2)
+                               try{
+                                       minDistance=Float.parseFloat(args[2]);
+                               } catch (NumberFormatException e){
+                                       sendMessage(context, replyTo, cannot_parse_min_distance);
+                                       break;
+                               }
+                       else
+                               minDistance=0;
+                       Heavy.location(context, replyTo, provider, minTime, minDistance);
+                       break;
+
+               case NOLOCATION:
+                       Heavy.nolocation(context, replyTo);
+                       break;
+
+               case RINGER:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.RINGER));
+                               break;
+                       }
+                       if(args.length==0)
+                               Heavy.ringer(context, replyTo);
+                       else{
+                               try{
+                                       final RingerMode rm=RingerMode.valueOf(args[0].toUpperCase(Locale.ENGLISH));
+                                       switch(rm){
+                                       case NORMAL:
+                                               Heavy.ringer(context, replyTo, AudioManager.RINGER_MODE_NORMAL);
+                                               break;
+                                       case VIBRATE:
+                                               Heavy.ringer(context, replyTo, AudioManager.RINGER_MODE_VIBRATE);
+                                               break;
+                                       case SILENT:
+                                               Heavy.ringer(context, replyTo, AudioManager.RINGER_MODE_SILENT);
+                                               break;
+                                       }
+                               } catch (IllegalArgumentException e){
+                                       Utils.sendMessage(context, replyTo, invalid_ringer_mode_valid_values_are, join(", ",toNonNull(RingerMode.values())));
+                               }
+                       }
+                       break;
+
+               case NCFILE:
+                       if(args.length!=3){
+                               Heavy.help(context, replyTo, toNonNull(Command.NCFILE));
+                               break;
+                       }
+
+                       final int ncfilePort;
+                       try{
+                               ncfilePort=Integer.parseInt(args[2]);
+                       } catch (NumberFormatException e){
+                               sendMessage(context, replyTo, cannot_parse_port);
+                               break;
+                       }
+                       Heavy.ncfile(context, replyTo, toNonNull(args[0]), toNonNull(args[1]), ncfilePort);
+                       break;
+
+               case PHOTO:
+                       if(args.length!=2){
+                               Heavy.help(context, replyTo, toNonNull(Command.PHOTO));
+                               break;
+                       }
+                       final int photoPort;
+                       try{
+                               photoPort=Integer.parseInt(args[1]);
+                       } catch (NumberFormatException e){
+                               sendMessage(context, replyTo, cannot_parse_port);
+                               break;
+                       }
+                       Heavy.photo(context, replyTo, toNonNull(args[0]), photoPort);
+                       break;
+
+               case SETNOTIFICATION:
+                       if(args.length!=1){
+                               Heavy.help(context, replyTo, toNonNull(Command.SETNOTIFICATION));
+                               break;
+                       }
+
+                       try{
+                               PreferenceManager.getDefaultSharedPreferences(context).edit()
+                               .putString(MessageType.valueOf(args[0].toUpperCase(Locale.ENGLISH)).toString(), replyTo.toString())
+                               .commit();
+                               sendMessage(context, replyTo, notification_enabled);
+                       } catch (IllegalArgumentException e){
+                               sendMessage(context, replyTo, messagetype_should_be_one_of, join(", ",toNonNull(MessageType.values())));
+                               break;
+                       }
+
+                       break;
+
+               case DELNOTIFICATION:
+                       if(args.length!=1){
+                               Heavy.help(context, replyTo, toNonNull(Command.DELNOTIFICATION));
+                               break;
+                       }
+
+                       try{
+                               PreferenceManager.getDefaultSharedPreferences(context).edit()
+                               .remove(MessageType.valueOf(args[0].toUpperCase(Locale.ENGLISH)).toString())
+                               .commit();
+                               sendMessage(context, replyTo,  notification_disabled);
+                       } catch (IllegalArgumentException e){
+                               sendMessage(context, replyTo,  messagetype_should_be_one_of, join(", ",toNonNull(MessageType.values())));
+                               break;
+                       }
+
+                       
+                       break;
+               case SETPASSWORD:
+                       if(args.length > 1){
+                               Heavy.help(context, replyTo, toNonNull(Command.SETPASSWORD));
+                               break;
+                       }
+
+                       try{
+                               if(args.length==0)
+                                       Heavy.setPassword(context, replyTo);
+                               else
+                                       Heavy.setPassword(context, replyTo, toNonNull(args[0]));
+                       } catch (SecurityException e){
+                               sendMessage(context, replyTo,  security_exception+e.getMessage());
+                       }
+                       break;
+
+               case WIPE:
+                       if(args.length!=2){
+                               Heavy.help(context, replyTo, toNonNull(Command.WIPE));
+                               break;
+                       }
+
+                       if(!args[1].equalsIgnoreCase(WIPE_CONFIRM_STRING)){
+                               sendMessage(context, replyTo, the_second_argument_to_wipe_must_be, WIPE_CONFIRM_STRING);
+                               break;
+                       }
+
+                       try{
+                               Heavy.wipe(context, toNonNull(WipeType.valueOf(args[0].toUpperCase(Locale.ENGLISH))));
+                       } catch (IllegalArgumentException e){
+                               sendMessage(context, replyTo, wipetype_should_be_one_of, join (", ",toNonNull(WipeType.values())));
+                       } catch (SecurityException e){
+                               sendMessage(context, replyTo, security_exception, e.getMessage());
+                       }
+                       break;
+
+               case LOCK:
+                       try{
+                               Heavy.lock(context, replyTo);
+                       } catch (SecurityException e){
+                               sendMessage(context, replyTo, security_exception, e.getMessage());
+                       }
+                       break;
+
+               case VIEW:
+                       if(args.length!=1){
+                               Heavy.help(context, replyTo, toNonNull(Command.VIEW));
+                               break;
+                       }
+
+                       Heavy.view(context, replyTo, toNonNull(Uri.parse(args[0])));
+                       break;
+
+               case PLAY:
+                       Heavy.musicPlayerCommand(context, replyTo, "play");
+                       break;
+
+               case PAUSE:
+                       Heavy.musicPlayerCommand(context, replyTo, "pause");
+                       break;
+
+               case NEXT:
+                       Heavy.musicPlayerCommand(context, replyTo, "next");
+                       break;
+
+               case PREV:
+                       Heavy.musicPlayerCommand(context, replyTo, "previous");
+                       break;
+
+               case BATT:
+                       Heavy.batt(context, replyTo);
+                       break;
+
+               case CALLLOG:
+                       if (args.length > 1) {
+                               Heavy.help(context, replyTo, toNonNull(Command.CALLLOG));
+                               break;
+                       }
+
+                       if (args.length == 0)
+                               Heavy.calllog(context, replyTo, 5);
+                       else {
+                               try {
+                                       Heavy.calllog(context, replyTo, Integer.parseInt(args[0]));
+                               } catch (IllegalArgumentException e){
+                                       sendMessage(context, replyTo, cannot_parse_count);
+                               }
+                       }
+                       break;
+
+               case SMSLOG:
+                       if (args.length > 1) {
+                               Heavy.help(context, replyTo, toNonNull(Command.SMSLOG));
+                               break;
+                       }
+
+                       if (args.length == 0)
+                               Heavy.smslog(context, replyTo, 5);
+                       else {
+                               try {
+                                       Heavy.smslog(context, replyTo, Integer.parseInt(args[0]));
+                               } catch (IllegalArgumentException e) {
+                                       sendMessage(context, replyTo, cannot_parse_count);
+                               }
+                       }
+                       break;
+
+               case HELP:
+                       if(args.length != 1){
+                               Heavy.help(context, replyTo, toNonNull(Command.HELP));
+                               break;
+                       }
+
+                       try {
+                               Heavy.help(context, replyTo, toNonNull(Command.valueOf(args[0].toUpperCase(Locale.ENGLISH))));
+                       } catch (IllegalArgumentException e) {
+                               sendMessage(context, replyTo, no_such_command_command_list, join(", ", toNonNull(Command.values())));
+                       }
+                       break;
+
+               case LS:
+                       if(args.length != 1){
+                               Heavy.help(context, replyTo, toNonNull(Command.LS));
+                               break;
+                       }
+
+                       Heavy.ls(context, replyTo, toNonNull(args[0]));
+                       break;
+
+               case RM:
+                       if(args.length != 1){
+                               Heavy.help(context, replyTo, toNonNull(Command.RM));
+                               break;
+                       }
+
+                       Heavy.rm(context, replyTo, toNonNull(args[0]));
+                       break;
+
+               case CONTACTS:
+                       if(args.length != 1){
+                               Heavy.help(context, replyTo, toNonNull(Command.CONTACTS));
+                               break;
+                       }
+
+                       Heavy.contacts(context, replyTo, toNonNull(args[0]));
+                       break;
+
+               case DISABLE:
+                       if(replyTo.protocol != Protocol.LOCAL || args.length != 1){
+                               Heavy.help(context, replyTo, toNonNull(Command.DISABLE));
+                               break;
+                       }
+
+                       try{
+                               Heavy.disable(context, replyTo, toNonNull(Command.valueOf(args[0].toUpperCase(Locale.ENGLISH))));
+                       } catch (IllegalArgumentException e){
+                               sendMessage(context, replyTo, no_such_command_command_list, join(", ", toNonNull(Command.values())));
+                       }
+                       break;
+
+               case ENABLE:
+                       if(replyTo.protocol != Protocol.LOCAL || args.length != 1){
+                               Heavy.help(context, replyTo, toNonNull(Command.ENABLE));
+                               break;
+                       }
+
+                       try{
+                               Heavy.enable(context, replyTo, toNonNull(Command.valueOf(args[0].toUpperCase(Locale.ENGLISH))));
+                       } catch (IllegalArgumentException e){
+                               sendMessage(context, replyTo, no_such_command_command_list, join(", ", toNonNull(Command.values())));
+                       }
+                       break;
+
+               case POLL:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.POLL));
+                               break;
+                       }
+
+                       if(args.length==0){
+                               Heavy.poll(context, replyTo);
+                               break;
+                       }
+
+                       final long interval;
+                       try{
+                               interval=Long.parseLong(args[0]);
+                       } catch(NumberFormatException e){
+                               sendMessage(context, replyTo, cannot_parse_interval);
+                               break;
+                       }
+
+                       Heavy.poll(context, replyTo, interval);
+                       break;
+
+               case HANGUP:
+                       Heavy.hangup(context, replyTo);
+                       break;
+
+               case ANSWER:
+                       Heavy.answer(context, replyTo);
+                       break;
+
+               case LAUNCH:
+                       if(args.length!=1){
+                               Heavy.help(context, replyTo, toNonNull(Command.LAUNCH));
+                               break;
+                       }
+                       Heavy.launch(context, replyTo, toNonNull(args[0]));
+                       break;
+
+               case DATA:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.DATA));
+                               break;
+                       }
+
+                       if(args.length==0){
+                               Heavy.data(context, replyTo);
+                               break;
+                       }
+                       try {
+                               Heavy.data(context, replyTo, OnOff.valueOf(args[0].toUpperCase(Locale.ENGLISH)) == OnOff.ON);
+                       } catch(IllegalArgumentException e) {
+                               sendMessage(context, replyTo, could_not_parse_argument_allowed_values_are, join(", ", toNonNull(OnOff.values())));
+                       }
+                       break;
+
+               case GPS:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.GPS));
+                               break;
+                       }
+
+                       if(args.length==0){
+                               Heavy.gps(context, replyTo);
+                               break;
+                       }
+
+                       try {
+                               Heavy.gps(context, replyTo, OnOff.valueOf(args[0].toUpperCase(Locale.ENGLISH)) == OnOff.ON);
+                       } catch(IllegalArgumentException e) {
+                               sendMessage(context, replyTo, could_not_parse_argument_allowed_values_are, join(", ", toNonNull(OnOff.values())));
+                       }
+                       break;
+
+               case GLOCATION:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.GLOCATION));
+                               break;
+                       }
+
+                       if(args.length==0){
+                               Heavy.glocation(context, replyTo);
+                               break;
+                       }
+
+                       try {
+                               Heavy.glocation(context, replyTo, OnOff.valueOf(args[0].toUpperCase(Locale.ENGLISH)) == OnOff.ON);
+                       } catch(IllegalArgumentException e) {
+                               sendMessage(context, replyTo, could_not_parse_argument_allowed_values_are, join(", ", toNonNull(OnOff.values())));
+                       }
+                       break;
+
+               case REBOOT:
+                       if(args.length>1){
+                               Heavy.help(context, replyTo, toNonNull(Command.REBOOT));
+                               break;
+                       }
+
+                       Heavy.reboot(context, replyTo, args.length==0?null:args[0]);
+                       break;
+
+               case SHUTDOWN:
+                       //TODO: implement command
+                       break;
+
+               case NOTIFY:
+                       if(args.length!=1 && args.length!=3){
+                               Heavy.help(context, replyTo, toNonNull(Command.NOTIFY));
+                       }
+
+                       final int id;
+                       try{
+                               id=Integer.parseInt(args[0]);
+                       } catch (NumberFormatException e){
+                               sendMessage(context, replyTo, R.string.could_not_parse_id);
+                               break;
+                       }
+
+                       if(args.length==1)
+                               Heavy.notify(context, replyTo, id);
+                       else
+                               Heavy.notify(context, replyTo, id, toNonNull(args[1]), toNonNull(args[2]));
+                       break;
+               }
+
+       }
+
+       /**
+        * Executes a given command
+        *
+        * @param context Context instance
+        * @param cmd Command to execute
+        * @param args arguments for the command
+        * @param replyTo Address to send replies to
+        */
+       public static void processCommand(final Context context, final String cmd,final String[] args,final Address replyTo){
+               final @NonNull Command command;
+               try{
+                       command=toNonNull(Command.valueOf(cmd.toUpperCase(Locale.ENGLISH)));
+               } catch (IllegalArgumentException e){
+                       sendMessage(context, replyTo, unknown_command, cmd.toUpperCase(Locale.ENGLISH), e.getMessage());
+                       return;
+               }
+
+               try{
+                       processCommand(context, command,args,replyTo);
+               } catch(Exception e){
+                       sendMessage(context, replyTo, error_while_processing_command, e.getClass().getName(), e.getMessage());
+                       Log.w(Utils.class.getName(), "Error while processing command", e);
+               }
+       }
+}
diff --git a/src/ro/ieval/fonbot/package-info.java b/src/ro/ieval/fonbot/package-info.java
new file mode 100644 (file)
index 0000000..484a74a
--- /dev/null
@@ -0,0 +1,26 @@
+
+/**
+ * The main package of FonBot
+ */
+@NonNullByDefault
+package ro.ieval.fonbot;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/*
+ * Copyright © 2013 Marius Gavrilescu
+ * 
+ * This file is part of FonBot.
+ *
+ * FonBot is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FonBot is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with FonBot.  If not, see <http://www.gnu.org/licenses/>. 
+ */
\ No newline at end of file
This page took 0.144702 seconds and 4 git commands to generate.