Merge m-c to m-i, a=merge

This commit is contained in:
Phil Ringnalda 2014-09-01 19:29:13 -07:00
commit c9f7cee60a
100 changed files with 3903 additions and 748 deletions

View File

@ -12,17 +12,36 @@
#include "mozilla/dom/ChildIterator.h"
#include "mozilla/dom/Element.h"
using namespace mozilla;
using namespace mozilla::a11y;
////////////////////////////////////////////////////////////////////////////////
// WalkState
////////////////////////////////////////////////////////////////////////////////
namespace mozilla {
namespace a11y {
struct WalkState
{
WalkState(nsIContent *aContent, uint32_t aFilter) :
content(aContent), prevState(nullptr), iter(aContent, aFilter) {}
nsCOMPtr<nsIContent> content;
WalkState *prevState;
dom::AllChildrenIterator iter;
};
} // namespace a11y
} // namespace mozilla
////////////////////////////////////////////////////////////////////////////////
// TreeWalker
////////////////////////////////////////////////////////////////////////////////
TreeWalker::
TreeWalker(Accessible* aContext, nsIContent* aContent, uint32_t aFlags) :
mDoc(aContext->Document()), mContext(aContext), mAnchorNode(aContent),
mFlags(aFlags)
mDoc(aContext->Document()), mContext(aContext),
mFlags(aFlags), mState(nullptr)
{
NS_ASSERTION(aContent, "No node for the accessible tree walker!");
@ -31,13 +50,17 @@ TreeWalker::
mChildFilter |= nsIContent::eSkipPlaceholderContent;
if (aContent)
PushState(aContent);
mState = new WalkState(aContent, mChildFilter);
MOZ_COUNT_CTOR(TreeWalker);
}
TreeWalker::~TreeWalker()
{
// Clear state stack from memory
while (mState)
PopState();
MOZ_COUNT_DTOR(TreeWalker);
}
@ -45,60 +68,74 @@ TreeWalker::~TreeWalker()
// TreeWalker: private
Accessible*
TreeWalker::NextChild()
TreeWalker::NextChildInternal(bool aNoWalkUp)
{
if (mStateStack.IsEmpty())
if (!mState || !mState->content)
return nullptr;
dom::AllChildrenIterator* top = &mStateStack[mStateStack.Length() - 1];
while (top) {
while (nsIContent* childNode = top->GetNextChild()) {
bool isSubtreeHidden = false;
Accessible* accessible = mFlags & eWalkCache ?
mDoc->GetAccessible(childNode) :
GetAccService()->GetOrCreateAccessible(childNode, mContext,
&isSubtreeHidden);
while (nsIContent* childNode = mState->iter.GetNextChild()) {
bool isSubtreeHidden = false;
Accessible* accessible = mFlags & eWalkCache ?
mDoc->GetAccessible(childNode) :
GetAccService()->GetOrCreateAccessible(childNode, mContext,
&isSubtreeHidden);
if (accessible)
return accessible;
// Walk down into subtree to find accessibles.
if (!isSubtreeHidden && childNode->IsElement()) {
PushState(childNode);
accessible = NextChildInternal(true);
if (accessible)
return accessible;
// Walk down into subtree to find accessibles.
if (!isSubtreeHidden && childNode->IsElement())
top = PushState(childNode);
}
top = PopState();
}
// No more children, get back to the parent.
nsIContent* anchorNode = mState->content;
PopState();
if (aNoWalkUp)
return nullptr;
if (mState)
return NextChildInternal(false);
// If we traversed the whole subtree of the anchor node. Move to next node
// relative anchor node within the context subtree if possible.
if (mFlags != eWalkContextTree)
return nullptr;
nsINode* contextNode = mContext->GetNode();
while (mAnchorNode != contextNode) {
nsINode* parentNode = mAnchorNode->GetFlattenedTreeParent();
while (anchorNode != mContext->GetNode()) {
nsINode* parentNode = anchorNode->GetFlattenedTreeParent();
if (!parentNode || !parentNode->IsElement())
return nullptr;
nsIContent* parent = parentNode->AsElement();
top = mStateStack.AppendElement(dom::AllChildrenIterator(parent,
mChildFilter));
while (nsIContent* childNode = top->GetNextChild()) {
if (childNode == mAnchorNode) {
mAnchorNode = parent;
return NextChild();
}
PushState(parentNode->AsElement());
while (nsIContent* childNode = mState->iter.GetNextChild()) {
if (childNode == anchorNode)
return NextChildInternal(false);
}
PopState();
anchorNode = parentNode->AsElement();
}
return nullptr;
}
dom::AllChildrenIterator*
void
TreeWalker::PopState()
{
size_t length = mStateStack.Length();
mStateStack.RemoveElementAt(length - 1);
return mStateStack.IsEmpty() ? nullptr : &mStateStack[mStateStack.Length() - 1];
WalkState* prevToLastState = mState->prevState;
delete mState;
mState = prevToLastState;
}
void
TreeWalker::PushState(nsIContent* aContent)
{
WalkState* nextToLastState = new WalkState(aContent, mChildFilter);
nextToLastState->prevState = mState;
mState = nextToLastState;
}

View File

@ -8,8 +8,6 @@
#include "mozilla/Attributes.h"
#include <stdint.h>
#include "mozilla/dom/ChildIterator.h"
#include "nsCOMPtr.h"
class nsIContent;
@ -19,6 +17,8 @@ namespace a11y {
class Accessible;
class DocAccessible;
struct WalkState;
/**
* This class is used to walk the DOM tree to create accessible tree.
*/
@ -50,36 +50,43 @@ public:
* rejected during tree creation then the caller should be unbind it
* from the document.
*/
Accessible* NextChild();
Accessible* NextChild()
{
return NextChildInternal(false);
}
private:
TreeWalker();
TreeWalker(const TreeWalker&);
TreeWalker& operator =(const TreeWalker&);
/**
* Return the next child accessible.
*
* @param aNoWalkUp [in] specifies the walk direction, true means we
* shouldn't go up through the tree if we failed find
* accessible children.
*/
Accessible* NextChildInternal(bool aNoWalkUp);
/**
* Create new state for the given node and push it on top of stack.
*
* @note State stack is used to navigate up/down the DOM subtree during
* accessible children search.
*/
dom::AllChildrenIterator* PushState(nsIContent* aContent)
{
return mStateStack.AppendElement(dom::AllChildrenIterator(aContent,
mChildFilter));
}
void PushState(nsIContent* aNode);
/**
* Pop state from stack.
*/
dom::AllChildrenIterator* PopState();
void PopState();
DocAccessible* mDoc;
Accessible* mContext;
nsIContent* mAnchorNode;
nsAutoTArray<dom::AllChildrenIterator, 20> mStateStack;
int32_t mChildFilter;
uint32_t mFlags;
WalkState* mState;
};
} // namespace a11y

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="4b4336c73c081b39776d399835ce4853aee5cc1c">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>

View File

@ -19,13 +19,13 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
<project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>

View File

@ -17,10 +17,10 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="4b4336c73c081b39776d399835ce4853aee5cc1c">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>

View File

@ -19,13 +19,13 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
<project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="4b4336c73c081b39776d399835ce4853aee5cc1c">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>

View File

@ -17,10 +17,10 @@
</project>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->
@ -122,7 +122,7 @@
<!-- Flame specific things -->
<project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="e8a318f7690092e639ba88891606f4183e846d3f"/>
<project name="device/qcom/common" path="device/qcom/common" revision="878804e0becfe5635bb8ccbf2671333d546c6fb6"/>
<project name="device-flame" path="device/t2m/flame" remote="b2g" revision="7dfad27ab7119ce820fc12f9e8029f0b73df4011"/>
<project name="device-flame" path="device/t2m/flame" remote="b2g" revision="f686fa63e8766a4799cffab0b072c7b80194c4fc"/>
<project name="codeaurora_kernel_msm" path="kernel" remote="b2g" revision="ebb14165369f5edc3f335d5bde6eef8439073589"/>
<project name="kernel_lk" path="bootable/bootloader/lk" remote="b2g" revision="9eb619d2efdf4bd121587d8296f5c10481f750b8"/>
<project name="platform_bootable_recovery" path="bootable/recovery" remote="b2g" revision="e81502511cda303c803e63f049574634bc96f9f2"/>

View File

@ -4,6 +4,6 @@
"remote": "",
"branch": ""
},
"revision": "bce92f813f48346b36ce9dffc16b0c93d0ac8330",
"revision": "14400ee07e836d74039e2317feebc0a13fcb60c8",
"repo_path": "/integration/gaia-central"
}

View File

@ -17,12 +17,12 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
<project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>

View File

@ -15,7 +15,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -17,10 +17,10 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

View File

@ -17,12 +17,12 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e7d31f0e9b6b19d9b484eeec8fb980718bc40d79"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="44bf2e3bc5ddea9db9a8c851bd353cb234aa883c"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="320650844ec7cba40a70317b761b88b47a8dca0e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7ddb07033043613303061416882c9b02ac3d76b6"/>
<project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>

View File

@ -1579,8 +1579,8 @@ pref("shumway.disabled", true);
// (This is intentionally on the high side; see bug 746055.)
pref("image.mem.max_decoded_image_kb", 256000);
// Enable by default on nightly and aurora.
#ifndef RELEASE_BUILD
// Enable by default development builds up until early beta
#ifdef EARLY_BETA_OR_EARLIER
pref("loop.enabled", true);
#else
pref("loop.enabled", false);

View File

@ -13,6 +13,11 @@ XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/Panel
(function() {
LoopUI = {
get toolbarButton() {
delete this.toolbarButton;
return this.toolbarButton = CustomizableUI.getWidget("loop-call-button").forWindow(window);
},
/**
* Opens the panel for Loop and sizes it appropriately.
*
@ -35,13 +40,40 @@ XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/Panel
* Triggers the initialization of the loop service. Called by
* delayedStartup.
*/
initialize: function() {
init: function() {
if (!Services.prefs.getBoolPref("loop.enabled")) {
CustomizableUI.getWidget("loop-call-button").forWindow(window).node.hidden = true;
this.toolbarButton.node.hidden = true;
return;
}
// Add observer notifications before the service is initialized
Services.obs.addObserver(this, "loop-status-changed", false);
MozLoopService.initialize();
this.updateToolbarState();
},
uninit: function() {
Services.obs.removeObserver(this, "loop-status-changed");
},
// Implements nsIObserver
observe: function(subject, topic, data) {
if (topic != "loop-status-changed") {
return;
}
this.updateToolbarState();
},
updateToolbarState: function() {
let state = "";
if (MozLoopService.errors.size) {
state = "error";
} else if (MozLoopService.doNotDisturb) {
state = "disabled";
}
this.toolbarButton.node.setAttribute("state", state);
},
};
})();

View File

@ -1213,7 +1213,7 @@ var gBrowserInit = {
gDataNotificationInfoBar.init();
#endif
LoopUI.initialize();
LoopUI.init();
gBrowserThumbnails.init();
@ -1384,6 +1384,7 @@ var gBrowserInit = {
TabView.uninit();
SocialUI.uninit();
gBrowserThumbnails.uninit();
LoopUI.uninit();
FullZoom.destroy();
Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");

View File

@ -436,7 +436,9 @@ let LoopContactsInternal = Object.freeze({
}
request.onsuccess = event => {
eventEmitter.emit("remove", contact);
if (contact) {
eventEmitter.emit("remove", contact);
}
callback(null, event.target.result);
};
request.onerror = event => callback(event.target.error);
@ -674,6 +676,7 @@ let LoopContactsInternal = Object.freeze({
if (!contact) {
callback(new Error("Contact with " + kKeyPath + " '" +
guid + "' could not be found"));
return;
}
LoopStorage.getStore(kObjectStoreName, (err, store) => {
@ -723,6 +726,7 @@ let LoopContactsInternal = Object.freeze({
if (!contact) {
callback(new Error("Contact with " + kKeyPath + " '" +
guid + "' could not be found"));
return;
}
contact.blocked = true;
@ -749,6 +753,7 @@ let LoopContactsInternal = Object.freeze({
if (!contact) {
callback(new Error("Contact with " + kKeyPath + " '" +
guid + "' could not be found"));
return;
}
contact.blocked = false;

View File

@ -61,6 +61,7 @@ let gInitializeTimer = null;
let gFxAOAuthClientPromise = null;
let gFxAOAuthClient = null;
let gFxAOAuthTokenData = null;
let gErrors = new Map();
/**
* Internal helper methods and state
@ -135,6 +136,30 @@ let MozLoopServiceInternal = {
*/
set doNotDisturb(aFlag) {
Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
this.notifyStatusChanged();
},
notifyStatusChanged: function() {
Services.obs.notifyObservers(null, "loop-status-changed", null);
},
/**
* @param {String} errorType a key to identify the type of error. Only one
* error of a type will be saved at a time.
* @param {Object} error an object describing the error in the format from Hawk errors
*/
setError: function(errorType, error) {
gErrors.set(errorType, error);
this.notifyStatusChanged();
},
clearError: function(errorType) {
gErrors.delete(errorType);
this.notifyStatusChanged();
},
get errors() {
return gErrors;
},
/**
@ -257,6 +282,7 @@ let MozLoopServiceInternal = {
if (!this.storeSessionToken(response.headers))
return;
this.clearError("registration");
gRegisteredDeferred.resolve();
// No need to clear the promise here, everything was good, so we don't need
// to re-register.
@ -278,6 +304,7 @@ let MozLoopServiceInternal = {
// XXX Bubble the precise details up to the UI somehow (bug 1013248).
Cu.reportError("Failed to register with the loop server. error: " + error);
this.setError("registration", error);
gRegisteredDeferred.reject(error.errno);
gRegisteredDeferred = null;
}
@ -301,7 +328,7 @@ let MozLoopServiceInternal = {
Services.prefs.setCharPref("loop.seenToS", "seen");
this.openChatWindow(null,
this.localizedStrings["incoming_call_title"].textContent,
this.localizedStrings["incoming_call_title2"].textContent,
"about:loopconversation#incoming/" + version);
},
@ -695,6 +722,10 @@ this.MozLoopService = {
MozLoopServiceInternal.doNotDisturb = aFlag;
},
get errors() {
return MozLoopServiceInternal.errors;
},
/**
* Returns the current locale
*

View File

@ -87,7 +87,7 @@ loop.conversation = (function(OT, mozL10n) {
});
return (
React.DOM.div({className: conversationPanelClass},
React.DOM.h2(null, __("incoming_call")),
React.DOM.h2(null, __("incoming_call_title2")),
React.DOM.div({className: "btn-group incoming-call-action-group"},
React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}),
@ -98,7 +98,7 @@ loop.conversation = (function(OT, mozL10n) {
React.DOM.button({className: btnClassDecline,
onClick: this._handleDecline},
__("incoming_call_decline_button")
__("incoming_call_cancel_button")
),
React.DOM.div({className: "btn-chevron",
onClick: this._toggleDeclineMenu}
@ -107,7 +107,7 @@ loop.conversation = (function(OT, mozL10n) {
React.DOM.ul({className: dropdownMenuClassesDecline},
React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock},
__("incoming_call_decline_and_block_button")
__("incoming_call_cancel_and_block_button")
)
)
@ -121,14 +121,14 @@ loop.conversation = (function(OT, mozL10n) {
React.DOM.button({className: btnClassAccept,
onClick: this._handleAccept("audio-video")},
React.DOM.span({className: "fx-embedded-answer-btn-text"},
__("incoming_call_answer_button")
__("incoming_call_accept_button")
),
React.DOM.span({className: "fx-embedded-btn-icon-video"}
)
),
React.DOM.div({className: "call-audio-only",
onClick: this._handleAccept("audio"),
title: __("incoming_call_answer_audio_only_tooltip")}
title: __("incoming_call_accept_audio_only_tooltip")}
)
)
),
@ -334,7 +334,7 @@ loop.conversation = (function(OT, mozL10n) {
* Call has ended, display a feedback form.
*/
feedback: function() {
document.title = mozL10n.get("call_has_ended");
document.title = mozL10n.get("conversation_has_ended");
var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
"feedback.baseUrl");
@ -362,7 +362,7 @@ loop.conversation = (function(OT, mozL10n) {
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
document.title = mozL10n.get("incoming_call_title");
document.title = mozL10n.get("incoming_call_title2");
document.body.classList.add(loop.shared.utils.getTargetPlatform());

View File

@ -87,7 +87,7 @@ loop.conversation = (function(OT, mozL10n) {
});
return (
<div className={conversationPanelClass}>
<h2>{__("incoming_call")}</h2>
<h2>{__("incoming_call_title2")}</h2>
<div className="btn-group incoming-call-action-group">
<div className="fx-embedded-incoming-call-button-spacer"></div>
@ -98,7 +98,7 @@ loop.conversation = (function(OT, mozL10n) {
<button className={btnClassDecline}
onClick={this._handleDecline}>
{__("incoming_call_decline_button")}
{__("incoming_call_cancel_button")}
</button>
<div className="btn-chevron"
onClick={this._toggleDeclineMenu}>
@ -107,7 +107,7 @@ loop.conversation = (function(OT, mozL10n) {
<ul className={dropdownMenuClassesDecline}>
<li className="btn-block" onClick={this._handleDeclineBlock}>
{__("incoming_call_decline_and_block_button")}
{__("incoming_call_cancel_and_block_button")}
</li>
</ul>
@ -121,14 +121,14 @@ loop.conversation = (function(OT, mozL10n) {
<button className={btnClassAccept}
onClick={this._handleAccept("audio-video")}>
<span className="fx-embedded-answer-btn-text">
{__("incoming_call_answer_button")}
{__("incoming_call_accept_button")}
</span>
<span className="fx-embedded-btn-icon-video">
</span>
</button>
<div className="call-audio-only"
onClick={this._handleAccept("audio")}
title={__("incoming_call_answer_audio_only_tooltip")} >
title={__("incoming_call_accept_audio_only_tooltip")} >
</div>
</div>
</div>
@ -334,7 +334,7 @@ loop.conversation = (function(OT, mozL10n) {
* Call has ended, display a feedback form.
*/
feedback: function() {
document.title = mozL10n.get("call_has_ended");
document.title = mozL10n.get("conversation_has_ended");
var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
"feedback.baseUrl");
@ -362,7 +362,7 @@ loop.conversation = (function(OT, mozL10n) {
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
document.title = mozL10n.get("incoming_call_title");
document.title = mozL10n.get("incoming_call_title2");
document.body.classList.add(loop.shared.utils.getTargetPlatform());

View File

@ -22,14 +22,24 @@ loop.panel = (function(_, mozL10n) {
var router;
/**
* Availability drop down menu subview.
* Dropdown menu mixin.
* @type {Object}
*/
var AvailabilityDropdown = React.createClass({displayName: 'AvailabilityDropdown',
var DropdownMenuMixin = {
getInitialState: function() {
return {
doNotDisturb: navigator.mozLoop.doNotDisturb,
showMenu: false
};
return {showMenu: false};
},
_onBodyClick: function() {
this.setState({showMenu: false});
},
componentDidMount: function() {
document.body.addEventListener("click", this._onBodyClick);
},
componentWillUnmount: function() {
document.body.removeEventListener("click", this._onBodyClick);
},
showDropdownMenu: function() {
@ -38,6 +48,19 @@ loop.panel = (function(_, mozL10n) {
hideDropdownMenu: function() {
this.setState({showMenu: false});
}
};
/**
* Availability drop down menu subview.
*/
var AvailabilityDropdown = React.createClass({displayName: 'AvailabilityDropdown',
mixins: [DropdownMenuMixin],
getInitialState: function() {
return {
doNotDisturb: navigator.mozLoop.doNotDisturb
};
},
// XXX target event can either be the li, the span or the i tag
@ -69,7 +92,7 @@ loop.panel = (function(_, mozL10n) {
'status-available': !this.state.doNotDisturb
});
var availabilityDropdown = cx({
'dnd-menu': true,
'dropdown-menu': true,
'hide': !this.state.showMenu
});
var availabilityText = this.state.doNotDisturb ?
@ -77,7 +100,7 @@ loop.panel = (function(_, mozL10n) {
__("display_name_available_status");
return (
React.DOM.div({className: "do-not-disturb"},
React.DOM.div({className: "dropdown"},
React.DOM.p({className: "dnd-status", onClick: this.showDropdownMenu},
React.DOM.span(null, availabilityText),
React.DOM.i({className: availabilityStatus})
@ -85,12 +108,12 @@ loop.panel = (function(_, mozL10n) {
React.DOM.ul({className: availabilityDropdown,
onMouseLeave: this.hideDropdownMenu},
React.DOM.li({onClick: this.changeAvailability("available"),
className: "dnd-menu-item dnd-make-available"},
className: "dropdown-menu-item dnd-make-available"},
React.DOM.i({className: "status status-available"}),
React.DOM.span(null, __("display_name_available_status"))
),
React.DOM.li({onClick: this.changeAvailability("do-not-disturb"),
className: "dnd-menu-item dnd-make-unavailable"},
className: "dropdown-menu-item dnd-make-unavailable"},
React.DOM.i({className: "status status-dnd"}),
React.DOM.span(null, __("display_name_dnd_status"))
)
@ -109,7 +132,8 @@ loop.panel = (function(_, mozL10n) {
if (this.state.seenToS == "unseen") {
var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
var tosHTML = __("legal_text_and_links2", {
var tosHTML = __("legal_text_and_links3", {
"clientShortname": __("client_shortname_fallback"),
"terms_of_use": React.renderComponentToStaticMarkup(
React.DOM.a({href: terms_of_use_url, target: "_blank"},
__("legal_text_tos")
@ -129,6 +153,93 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* Panel settings (gear) menu entry.
*/
var SettingsDropdownEntry = React.createClass({displayName: 'SettingsDropdownEntry',
propTypes: {
onClick: React.PropTypes.func.isRequired,
label: React.PropTypes.string.isRequired,
icon: React.PropTypes.string,
displayed: React.PropTypes.bool
},
getDefaultProps: function() {
return {displayed: true};
},
render: function() {
if (!this.props.displayed) {
return null;
}
return (
React.DOM.li({onClick: this.props.onClick, className: "dropdown-menu-item"},
this.props.icon ?
React.DOM.i({className: "icon icon-" + this.props.icon}) :
null,
React.DOM.span(null, this.props.label)
)
);
}
});
/**
* Panel settings (gear) menu.
*/
var SettingsDropdown = React.createClass({displayName: 'SettingsDropdown',
mixins: [DropdownMenuMixin],
handleClickSettingsEntry: function() {
// XXX to be implemented
},
handleClickAccountEntry: function() {
// XXX to be implemented
},
handleClickAuthEntry: function() {
if (this._isSignedIn()) {
// XXX to be implemented - bug 979845
navigator.mozLoop.logOutFromFxA();
} else {
navigator.mozLoop.logInToFxA();
}
},
_isSignedIn: function() {
// XXX to be implemented - bug 979845
return !!navigator.mozLoop.loggedInToFxA;
},
render: function() {
var cx = React.addons.classSet;
return (
React.DOM.div({className: "settings-menu dropdown"},
React.DOM.a({className: "btn btn-settings", onClick: this.showDropdownMenu,
title: __("settings_menu_button_tooltip")}),
React.DOM.ul({className: cx({"dropdown-menu": true, hide: !this.state.showMenu}),
onMouseLeave: this.hideDropdownMenu},
SettingsDropdownEntry({label: __("settings_menu_item_settings"),
onClick: this.handleClickSettingsEntry,
icon: "settings"}),
SettingsDropdownEntry({label: __("settings_menu_item_account"),
onClick: this.handleClickAccountEntry,
icon: "account",
displayed: this._isSignedIn()}),
SettingsDropdownEntry({label: this._isSignedIn() ?
__("settings_menu_item_signout") :
__("settings_menu_item_signin"),
onClick: this.handleClickAuthEntry,
icon: this._isSignedIn() ? "signout" : "signin"})
)
)
);
}
});
/**
* Panel layout.
*/
var PanelLayout = React.createClass({displayName: 'PanelLayout',
propTypes: {
summary: React.PropTypes.string.isRequired
@ -207,8 +318,8 @@ loop.panel = (function(_, mozL10n) {
_generateMailTo: function() {
return encodeURI([
"mailto:?subject=" + __("share_email_subject2") + "&",
"body=" + __("share_email_body2", {callUrl: this.state.callUrl})
"mailto:?subject=" + __("share_email_subject3") + "&",
"body=" + __("share_email_body3", {callUrl: this.state.callUrl})
].join(""));
},
@ -259,6 +370,28 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* FxA sign in/up link component.
*/
var AuthLink = React.createClass({displayName: 'AuthLink',
handleSignUpLinkClick: function() {
navigator.mozLoop.logInToFxA();
},
render: function() {
if (navigator.mozLoop.loggedInToFxA) { // XXX to be implemented
return null;
}
return (
React.DOM.p({className: "signin-link"},
React.DOM.a({href: "#", onClick: this.handleSignUpLinkClick},
__("panel_footer_signin_or_signup_link")
)
)
);
}
});
/**
* Panel view.
*/
@ -270,10 +403,6 @@ loop.panel = (function(_, mozL10n) {
callUrl: React.PropTypes.string
},
handleSignUpLinkClick: function() {
navigator.mozLoop.logInToFxA();
},
render: function() {
return (
React.DOM.div(null,
@ -283,9 +412,8 @@ loop.panel = (function(_, mozL10n) {
ToSView(null),
React.DOM.div({className: "footer"},
AvailabilityDropdown(null),
React.DOM.a({className: "signin-link", href: "#", onClick: this.handleSignUpLinkClick},
__("panel_footer_signin_or_signup_link")
)
AuthLink(null),
SettingsDropdown(null)
)
)
);
@ -373,6 +501,7 @@ loop.panel = (function(_, mozL10n) {
Backbone.history.start();
document.body.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection());
// Notify the window that we've finished initalization and initial layout
var evtObject = document.createEvent('Event');
@ -386,6 +515,7 @@ loop.panel = (function(_, mozL10n) {
CallUrlResult: CallUrlResult,
PanelView: PanelView,
PanelRouter: PanelRouter,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView
};
})(_, document.mozL10n);

View File

@ -22,14 +22,24 @@ loop.panel = (function(_, mozL10n) {
var router;
/**
* Availability drop down menu subview.
* Dropdown menu mixin.
* @type {Object}
*/
var AvailabilityDropdown = React.createClass({
var DropdownMenuMixin = {
getInitialState: function() {
return {
doNotDisturb: navigator.mozLoop.doNotDisturb,
showMenu: false
};
return {showMenu: false};
},
_onBodyClick: function() {
this.setState({showMenu: false});
},
componentDidMount: function() {
document.body.addEventListener("click", this._onBodyClick);
},
componentWillUnmount: function() {
document.body.removeEventListener("click", this._onBodyClick);
},
showDropdownMenu: function() {
@ -38,6 +48,19 @@ loop.panel = (function(_, mozL10n) {
hideDropdownMenu: function() {
this.setState({showMenu: false});
}
};
/**
* Availability drop down menu subview.
*/
var AvailabilityDropdown = React.createClass({
mixins: [DropdownMenuMixin],
getInitialState: function() {
return {
doNotDisturb: navigator.mozLoop.doNotDisturb
};
},
// XXX target event can either be the li, the span or the i tag
@ -69,7 +92,7 @@ loop.panel = (function(_, mozL10n) {
'status-available': !this.state.doNotDisturb
});
var availabilityDropdown = cx({
'dnd-menu': true,
'dropdown-menu': true,
'hide': !this.state.showMenu
});
var availabilityText = this.state.doNotDisturb ?
@ -77,7 +100,7 @@ loop.panel = (function(_, mozL10n) {
__("display_name_available_status");
return (
<div className="do-not-disturb">
<div className="dropdown">
<p className="dnd-status" onClick={this.showDropdownMenu}>
<span>{availabilityText}</span>
<i className={availabilityStatus}></i>
@ -85,12 +108,12 @@ loop.panel = (function(_, mozL10n) {
<ul className={availabilityDropdown}
onMouseLeave={this.hideDropdownMenu}>
<li onClick={this.changeAvailability("available")}
className="dnd-menu-item dnd-make-available">
className="dropdown-menu-item dnd-make-available">
<i className="status status-available"></i>
<span>{__("display_name_available_status")}</span>
</li>
<li onClick={this.changeAvailability("do-not-disturb")}
className="dnd-menu-item dnd-make-unavailable">
className="dropdown-menu-item dnd-make-unavailable">
<i className="status status-dnd"></i>
<span>{__("display_name_dnd_status")}</span>
</li>
@ -109,7 +132,8 @@ loop.panel = (function(_, mozL10n) {
if (this.state.seenToS == "unseen") {
var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
var tosHTML = __("legal_text_and_links2", {
var tosHTML = __("legal_text_and_links3", {
"clientShortname": __("client_shortname_fallback"),
"terms_of_use": React.renderComponentToStaticMarkup(
<a href={terms_of_use_url} target="_blank">
{__("legal_text_tos")}
@ -129,6 +153,93 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* Panel settings (gear) menu entry.
*/
var SettingsDropdownEntry = React.createClass({
propTypes: {
onClick: React.PropTypes.func.isRequired,
label: React.PropTypes.string.isRequired,
icon: React.PropTypes.string,
displayed: React.PropTypes.bool
},
getDefaultProps: function() {
return {displayed: true};
},
render: function() {
if (!this.props.displayed) {
return null;
}
return (
<li onClick={this.props.onClick} className="dropdown-menu-item">
{this.props.icon ?
<i className={"icon icon-" + this.props.icon}></i> :
null}
<span>{this.props.label}</span>
</li>
);
}
});
/**
* Panel settings (gear) menu.
*/
var SettingsDropdown = React.createClass({
mixins: [DropdownMenuMixin],
handleClickSettingsEntry: function() {
// XXX to be implemented
},
handleClickAccountEntry: function() {
// XXX to be implemented
},
handleClickAuthEntry: function() {
if (this._isSignedIn()) {
// XXX to be implemented - bug 979845
navigator.mozLoop.logOutFromFxA();
} else {
navigator.mozLoop.logInToFxA();
}
},
_isSignedIn: function() {
// XXX to be implemented - bug 979845
return !!navigator.mozLoop.loggedInToFxA;
},
render: function() {
var cx = React.addons.classSet;
return (
<div className="settings-menu dropdown">
<a className="btn btn-settings" onClick={this.showDropdownMenu}
title={__("settings_menu_button_tooltip")} />
<ul className={cx({"dropdown-menu": true, hide: !this.state.showMenu})}
onMouseLeave={this.hideDropdownMenu}>
<SettingsDropdownEntry label={__("settings_menu_item_settings")}
onClick={this.handleClickSettingsEntry}
icon="settings" />
<SettingsDropdownEntry label={__("settings_menu_item_account")}
onClick={this.handleClickAccountEntry}
icon="account"
displayed={this._isSignedIn()} />
<SettingsDropdownEntry label={this._isSignedIn() ?
__("settings_menu_item_signout") :
__("settings_menu_item_signin")}
onClick={this.handleClickAuthEntry}
icon={this._isSignedIn() ? "signout" : "signin"} />
</ul>
</div>
);
}
});
/**
* Panel layout.
*/
var PanelLayout = React.createClass({
propTypes: {
summary: React.PropTypes.string.isRequired
@ -207,8 +318,8 @@ loop.panel = (function(_, mozL10n) {
_generateMailTo: function() {
return encodeURI([
"mailto:?subject=" + __("share_email_subject2") + "&",
"body=" + __("share_email_body2", {callUrl: this.state.callUrl})
"mailto:?subject=" + __("share_email_subject3") + "&",
"body=" + __("share_email_body3", {callUrl: this.state.callUrl})
].join(""));
},
@ -259,6 +370,28 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* FxA sign in/up link component.
*/
var AuthLink = React.createClass({
handleSignUpLinkClick: function() {
navigator.mozLoop.logInToFxA();
},
render: function() {
if (navigator.mozLoop.loggedInToFxA) { // XXX to be implemented
return null;
}
return (
<p className="signin-link">
<a href="#" onClick={this.handleSignUpLinkClick}>
{__("panel_footer_signin_or_signup_link")}
</a>
</p>
);
}
});
/**
* Panel view.
*/
@ -270,10 +403,6 @@ loop.panel = (function(_, mozL10n) {
callUrl: React.PropTypes.string
},
handleSignUpLinkClick: function() {
navigator.mozLoop.logInToFxA();
},
render: function() {
return (
<div>
@ -283,9 +412,8 @@ loop.panel = (function(_, mozL10n) {
<ToSView />
<div className="footer">
<AvailabilityDropdown />
<a className="signin-link" href="#" onClick={this.handleSignUpLinkClick}>
{__("panel_footer_signin_or_signup_link")}
</a>
<AuthLink />
<SettingsDropdown />
</div>
</div>
);
@ -373,6 +501,7 @@ loop.panel = (function(_, mozL10n) {
Backbone.history.start();
document.body.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection());
// Notify the window that we've finished initalization and initial layout
var evtObject = document.createEvent('Event');
@ -386,6 +515,7 @@ loop.panel = (function(_, mozL10n) {
CallUrlResult: CallUrlResult,
PanelView: PanelView,
PanelRouter: PanelRouter,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView
};
})(_, document.mozL10n);

View File

@ -74,6 +74,44 @@
margin-bottom: 0;
}
/* Dropdown menu (shared styles) */
.dropdown {
position: relative;
}
.dropdown-menu {
position: absolute;
top: -28px;
left: 0;
background: #fdfdfd;
box-shadow: 0 1px 3px rgba(0,0,0,.3);
list-style: none;
padding: 5px;
border-radius: 2px;
}
body[dir=rtl] .dropdown-menu-item {
left: auto;
right: 10px;
}
.dropdown-menu-item {
text-align: start;
margin: .3em 0;
padding: .2em .5em;
cursor: pointer;
border: 1px solid transparent;
border-radius: 2px;
font-size: 1em;
white-space: nowrap;
}
.dropdown-menu-item:hover {
border: 1px solid #ccc;
background: #eee;
}
/* DnD menu */
.dnd-status {
@ -89,37 +127,6 @@
background: #F1F1F1;
}
.do-not-disturb {
position: relative;
}
.dnd-menu {
position: absolute;
top: -28px;
left: 0;
background: #fdfdfd;
box-shadow: 0 1px 3px rgba(0,0,0,.3);
list-style: none;
padding: 5px;
border-radius: 2px;
}
.dnd-menu-item {
text-align: left;
margin: .3em 0;
padding: .2em .5em;
cursor: pointer;
border: 1px solid transparent;
border-radius: 2px;
font-size: 1em;
white-space: nowrap;
}
.dnd-menu-item:hover {
border: 1px solid #ccc;
background: #eee;
}
/* Status badges -- Available/Unavailable */
.status {
@ -141,12 +148,68 @@
/* Sign in/up link */
.signin-link {
display: none; /* XXX This should be removed as soon bugs 1047144 & 979845 land */
line-height: 100%;
display: none; /* XXX This should be displayed as soon bug 979845 lands */
flex: 2 1 auto;
margin-top: 14px;
border-right: 1px solid #aaa;
padding-right: 1em;
margin-right: 1em;
text-align: right;
}
.signin-link a {
font-size: .9em;
text-decoration: none;
color: #888;
margin-top: 16px;
}
/* Settings (gear) menu */
.btn-settings {
display: none; /* XXX This should be displayed as soon bug 979845 lands */
background: transparent url(../img/svg/glyph-settings-16x16.svg) no-repeat center center;
background-size: contain;
width: 12px;
height: 12px;
}
.footer .btn-settings {
margin-top: 17px; /* used to align the gear icon with the availability dropdown menu inner text */
opacity: .6; /* used to "grey" the icon a little */
}
.settings-menu .dropdown-menu {
/* The panel can't have dropdown menu overflowing its iframe boudaries;
let's anchor it from the bottom-right, while resetting the top & left values
set by .dropdown-menu */
top: auto;
left: auto;
bottom: -8px;
right: 14px;
}
.settings-menu .icon {
display: inline-block;
background-size: contain;
width: 12px;
height: 12px;
margin-right: 1em;
}
.settings-menu .icon-settings {
background: transparent url(../img/svg/glyph-settings-16x16.svg) no-repeat center center;
}
.settings-menu .icon-account {
background: transparent url(../img/svg/glyph-account-16x16.svg) no-repeat center center;
}
.settings-menu .icon-signin {
background: transparent url(../img/svg/glyph-signin-16x16.svg) no-repeat center center;
}
.settings-menu .icon-signout {
background: transparent url(../img/svg/glyph-signout-16x16.svg) no-repeat center center;
}
/* Terms of Service */
@ -181,4 +244,3 @@
padding: 14px;
margin-top: 14px;
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<g id="Contacts">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M8,6.526c1.802,0,3.263-1.461,3.263-3.263
C11.263,1.461,9.802,0,8,0C6.198,0,4.737,1.461,4.737,3.263C4.737,5.066,6.198,6.526,8,6.526z M14.067,11.421c0,0,0-0.001,0-0.001
c0-1.676-1.397-3.119-3.419-3.807L8.001,10.26L5.354,7.613C3.331,8.3,1.933,9.744,1.933,11.42v0.001H1.93
c0,1.679,0.328,3.246,0.896,4.579h10.348c0.568-1.333,0.896-2.9,0.896-4.579H14.067z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 904 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<path id="Setting" fill-rule="evenodd" clip-rule="evenodd" fill="#131311" d="M14.77,8c0,0.804,0.262,1.548,0.634,1.678L16,9.887
c-0.205,0.874-0.553,1.692-1.011,2.434l-0.567-0.272c-0.355-0.171-1.066,0.17-1.635,0.738c-0.569,0.569-0.909,1.279-0.738,1.635
l0.273,0.568c-0.741,0.46-1.566,0.79-2.438,0.998l-0.205-0.584c-0.13-0.372-0.874-0.634-1.678-0.634s-1.548,0.262-1.678,0.634
l-0.209,0.596c-0.874-0.205-1.692-0.553-2.434-1.011l0.272-0.567c0.171-0.355-0.17-1.066-0.739-1.635
c-0.568-0.568-1.279-0.909-1.635-0.738l-0.568,0.273c-0.46-0.741-0.79-1.566-0.998-2.439l0.584-0.205
C0.969,9.547,1.231,8.804,1.231,8c0-0.804-0.262-1.548-0.634-1.678L0,6.112c0.206-0.874,0.565-1.685,1.025-2.427l0.554,0.266
c0.355,0.171,1.066-0.17,1.635-0.738c0.569-0.568,0.909-1.28,0.739-1.635L3.686,1.025c0.742-0.46,1.553-0.818,2.427-1.024
l0.209,0.596C6.453,0.969,7.197,1.23,8.001,1.23s1.548-0.262,1.678-0.634l0.209-0.596c0.874,0.205,1.692,0.553,2.434,1.011
l-0.272,0.567c-0.171,0.355,0.17,1.066,0.738,1.635c0.569,0.568,1.279,0.909,1.635,0.738l0.568-0.273
c0.46,0.741,0.79,1.566,0.998,2.438l-0.584,0.205C15.032,6.452,14.77,7.196,14.77,8z M8.001,3.661C5.604,3.661,3.661,5.603,3.661,8
c0,2.397,1.943,4.34,4.339,4.34c2.397,0,4.339-1.943,4.339-4.34C12.34,5.603,10.397,3.661,8.001,3.661z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<g id="Outgoing_14x14_1_">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M9.921,8.415c0.105-0.11,0.146-0.265,0.13-0.432
c0.016-0.166-0.025-0.321-0.13-0.429L9.305,6.938l-2.6-2.65C6.402,3.973,5.973,3.906,5.748,4.139L5.238,4.68
c-0.225,0.233-0.16,0.679,0.144,0.995L6.44,6.754H0.608C0.272,6.754,0,7.026,0,7.361l0,1.215c0,0.335,0.272,0.607,0.608,0.607H6.47
l-1.136,1.155c-0.305,0.313-0.369,0.756-0.144,0.987L5.7,11.861c0.225,0.233,0.654,0.166,0.959-0.149l2.619-2.663L9.921,8.415z"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M14,0H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
c0,0.331,0.269,0.6,0.6,0.6H12.5C13.328,2,14,2.672,14,3.5v9c0,0.828-0.672,1.5-1.5,1.5H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
c0,0.331,0.269,0.6,0.6,0.6H14c1.105,0,2-0.895,2-2V2C16,0.895,15.105,0,14,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M14,0H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
c0,0.331,0.269,0.6,0.6,0.6H12.5C13.328,2,14,2.672,14,3.5v9c0,0.828-0.672,1.5-1.5,1.5H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
c0,0.331,0.269,0.6,0.6,0.6H14c1.105,0,2-0.895,2-2V2C16,0.895,15.105,0,14,0z"/>
<g id="Outgoing_14x14_2_">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M0.133,7.585c-0.105,0.11-0.146,0.265-0.13,0.432
c-0.016,0.166,0.025,0.321,0.13,0.429l0.616,0.615l2.6,2.65c0.304,0.315,0.732,0.382,0.958,0.149l0.51-0.541
c0.225-0.233,0.16-0.679-0.144-0.995L3.615,9.246h5.832c0.335,0,0.608-0.272,0.608-0.607V7.424c0-0.335-0.272-0.607-0.608-0.607
H3.585L4.72,5.662c0.305-0.313,0.369-0.756,0.144-0.987L4.355,4.139C4.13,3.906,3.701,3.973,3.396,4.287L0.777,6.951L0.133,7.585z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -184,7 +184,7 @@ loop.shared.views = (function(_, OT, l10n) {
React.DOM.li({className: "conversation-toolbar-btn-box"},
React.DOM.button({className: "btn btn-hangup", onClick: this.handleClickHangup,
title: l10n.get("hangup_button_title")},
l10n.get("hangup_button_caption")
l10n.get("hangup_button_caption2")
)
),
React.DOM.li({className: "conversation-toolbar-btn-box"},
@ -611,7 +611,7 @@ loop.shared.views = (function(_, OT, l10n) {
default:
return (
FeedbackLayout({title:
l10n.get("feedback_call_experience_heading")},
l10n.get("feedback_call_experience_heading2")},
React.DOM.div({className: "faces"},
React.DOM.button({className: "face face-happy",
onClick: this.handleHappyClick}),

View File

@ -184,7 +184,7 @@ loop.shared.views = (function(_, OT, l10n) {
<li className="conversation-toolbar-btn-box">
<button className="btn btn-hangup" onClick={this.handleClickHangup}
title={l10n.get("hangup_button_title")}>
{l10n.get("hangup_button_caption")}
{l10n.get("hangup_button_caption2")}
</button>
</li>
<li className="conversation-toolbar-btn-box">
@ -611,7 +611,7 @@ loop.shared.views = (function(_, OT, l10n) {
default:
return (
<FeedbackLayout title={
l10n.get("feedback_call_experience_heading")}>
l10n.get("feedback_call_experience_heading2")}>
<div className="faces">
<button className="face face-happy"
onClick={this.handleHappyClick}></button>

View File

@ -38,8 +38,12 @@ browser.jar:
content/browser/loop/shared/img/mute-inverse-14x14@2x.png (content/shared/img/mute-inverse-14x14@2x.png)
content/browser/loop/shared/img/video-inverse-14x14.png (content/shared/img/video-inverse-14x14.png)
content/browser/loop/shared/img/video-inverse-14x14@2x.png (content/shared/img/video-inverse-14x14@2x.png)
content/browser/loop/shared/img/dropdown-inverse.png (content/shared/img/dropdown-inverse.png)
content/browser/loop/shared/img/dropdown-inverse@2x.png (content/shared/img/dropdown-inverse@2x.png)
content/browser/loop/shared/img/dropdown-inverse.png (content/shared/img/dropdown-inverse.png)
content/browser/loop/shared/img/dropdown-inverse@2x.png (content/shared/img/dropdown-inverse@2x.png)
content/browser/loop/shared/img/svg/glyph-settings-16x16.svg (content/shared/img/svg/glyph-settings-16x16.svg)
content/browser/loop/shared/img/svg/glyph-account-16x16.svg (content/shared/img/svg/glyph-account-16x16.svg)
content/browser/loop/shared/img/svg/glyph-signin-16x16.svg (content/shared/img/svg/glyph-signin-16x16.svg)
content/browser/loop/shared/img/svg/glyph-signout-16x16.svg (content/shared/img/svg/glyph-signout-16x16.svg)
# Shared scripts
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)

View File

@ -55,8 +55,6 @@ body,
align-content: center;
}
.footer,
.footer a,
.terms-service,
.terms-service a {
font-size: .6rem;

View File

@ -73,7 +73,7 @@ loop.webapp = (function($, _, OT, webL10n) {
React.DOM.div({className: "info-panel"},
React.DOM.div({className: "firefox-logo"}),
React.DOM.h1(null, __("call_url_unavailable_notification_heading")),
React.DOM.h4(null, __("call_url_unavailable_notification_message"))
React.DOM.h4(null, __("call_url_unavailable_notification_message2"))
),
PromoteFirefoxView({helper: this.props.helper})
)
@ -250,7 +250,7 @@ loop.webapp = (function($, _, OT, webL10n) {
urlCreationDateString: this.state.urlCreationDateString}),
React.DOM.p({className: "standalone-call-btn-label"},
__("initiate_call_button_label")
__("initiate_call_button_label2")
),
React.DOM.div({id: "messages"}),
@ -264,9 +264,9 @@ loop.webapp = (function($, _, OT, webL10n) {
React.DOM.button({className: btnClassStartCall,
onClick: this._initiateOutgoingCall("audio-video"),
disabled: this.state.disableCallButton,
title: __("initiate_audio_video_call_tooltip")},
title: __("initiate_audio_video_call_tooltip2")},
React.DOM.span({className: "standalone-call-btn-text"},
__("initiate_audio_video_call_button")
__("initiate_audio_video_call_button2")
),
React.DOM.span({className: "standalone-call-btn-video-icon"})
),
@ -285,7 +285,7 @@ loop.webapp = (function($, _, OT, webL10n) {
React.DOM.button({className: "start-audio-only-call",
onClick: this._initiateOutgoingCall("audio"),
disabled: this.state.disableCallButton},
__("initiate_audio_call_button")
__("initiate_audio_call_button2")
)
)
)

View File

@ -73,7 +73,7 @@ loop.webapp = (function($, _, OT, webL10n) {
<div className="info-panel">
<div className="firefox-logo" />
<h1>{__("call_url_unavailable_notification_heading")}</h1>
<h4>{__("call_url_unavailable_notification_message")}</h4>
<h4>{__("call_url_unavailable_notification_message2")}</h4>
</div>
<PromoteFirefoxView helper={this.props.helper} />
</div>
@ -250,7 +250,7 @@ loop.webapp = (function($, _, OT, webL10n) {
urlCreationDateString={this.state.urlCreationDateString} />
<p className="standalone-call-btn-label">
{__("initiate_call_button_label")}
{__("initiate_call_button_label2")}
</p>
<div id="messages"></div>
@ -264,9 +264,9 @@ loop.webapp = (function($, _, OT, webL10n) {
<button className={btnClassStartCall}
onClick={this._initiateOutgoingCall("audio-video")}
disabled={this.state.disableCallButton}
title={__("initiate_audio_video_call_tooltip")} >
title={__("initiate_audio_video_call_tooltip2")} >
<span className="standalone-call-btn-text">
{__("initiate_audio_video_call_button")}
{__("initiate_audio_video_call_button2")}
</span>
<span className="standalone-call-btn-video-icon"></span>
</button>
@ -285,7 +285,7 @@ loop.webapp = (function($, _, OT, webL10n) {
<button className="start-audio-only-call"
onClick={this._initiateOutgoingCall("audio")}
disabled={this.state.disableCallButton} >
{__("initiate_audio_call_button")}
{__("initiate_audio_call_button2")}
</button>
</li>
</ul>

View File

@ -1,18 +1,26 @@
## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
[en]
call_has_ended=Your call has ended.
restart_call=Rejoin
conversation_has_ended=Your conversation has ended.
call_timeout_notification_text=Your call did not go through.
missing_conversation_info=Missing conversation information.
network_disconnected=The network connection terminated abruptly.
peer_ended_conversation2=The person you were calling has ended the conversation.
connection_error_see_console_notification=Call failed; see console for details.
generic_failure_title=Something went wrong.
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
retry_call_button=Retry
feedback_report_user_button=Report User
unable_retrieve_call_info=Unable to retrieve conversation information.
hangup_button_title=Hang up
hangup_button_caption=End Call
hangup_button_caption2=Exit
mute_local_audio_button_title=Mute your audio
unmute_local_audio_button_title=Unmute your audio
mute_local_video_button_title=Mute your video
unmute_local_video_button_title=Unmute your video
start_call=Start the call
outgoing_call_title=Start conversation?
call_with_contact_title=Conversation with {{incomingCallIdentity}}
welcome=Welcome to the {{clientShortname}} web client.
incompatible_browser=Incompatible Browser
powered_by_webrtc=The audio and video components of {{clientShortname}} are powered by WebRTC.
@ -22,14 +30,14 @@ sorry_device_unsupported=Sorry, {{clientShortname}} does not currently support y
use_firefox_windows_mac_linux=Please open this page using the latest {{brandShortname}} on Windows, Android, Mac or Linux.
connection_error_see_console_notification=Call failed; see console for details.
call_url_unavailable_notification_heading=Oops!
call_url_unavailable_notification_message=This URL is unavailable.
call_url_unavailable_notification_message2=Sorry, this URL is not available. It may be expired or entered incorrectly.
promote_firefox_hello_heading=Download {{brandShortname}} to make free audio and video calls!
get_firefox_button=Get {{brandShortname}}
call_url_unavailable_notification=This URL is unavailable.
initiate_call_button_label=Click Call to start a video chat
initiate_audio_video_call_button=Call
initiate_audio_video_call_tooltip=Start a video call
initiate_audio_call_button=Voice call
initiate_call_button_label2=Ready to start your conversation?
initiate_audio_video_call_button2=Start
initiate_audio_video_call_tooltip2=Start a video conversation
initiate_audio_call_button2=Voice conversation
reject_incoming_call=Cancel
legal_text_and_links=By using this product you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}
terms_of_use_link_text=Terms of use
privacy_notice_link_text=Privacy notice
@ -37,9 +45,10 @@ brandShortname=Firefox
clientShortname=WebRTC!
## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
call_url_creation_date_label=(from {{call_url_creation_date}})
call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
[fr]
call_has_ended=L'appel est terminé.
call_timeout_notification_text=Votre appel n'a pas abouti.
missing_conversation_info=Informations de communication manquantes.
network_disconnected=La connexion réseau semble avoir été interrompue.
@ -50,7 +59,6 @@ mute_local_audio_button_title=Couper la diffusion audio
unmute_local_audio_button_title=Reprendre la diffusion audio
mute_local_video_button_title=Couper la diffusion vidéo
unmute_local_video_button_title=Reprendre la diffusion vidéo
start_call=Démarrer l'appel
welcome=Bienvenue sur {{clientShortname}}.
incompatible_browser=Navigateur non supporté
powered_by_webrtc=Les fonctionnalités audio et vidéo de {{clientShortname}} utilisent WebRTC.
@ -59,6 +67,5 @@ incompatible_device=Plateforme non supportée
sorry_device_unsupported=Désolé, {{clientShortname}} ne fonctionne actuellement pas sur votre appareil.
use_firefox_windows_mac_linux=Merci d'ouvrir cette page avec une version récente de {{brandShortname}} pour Windows, Android, Mac ou Linux.
call_url_unavailable_notification_heading=Oups !
call_url_unavailable_notification_message=Cette URL n'est pas disponible.
promote_firefox_hello_heading=Téléchargez {{brandShortname}} pour passer des appels audio et vidéo gratuitement !
get_firefox_button=Téléchargez {{brandShortname}}

View File

@ -217,18 +217,90 @@ describe("loop.panel", function() {
}));
});
describe("FxA sign in/up link", function() {
describe("AuthLink", function() {
it("should trigger the FxA sign in/up process when clicking the link",
function() {
navigator.mozLoop.loggedInToFxA = false;
navigator.mozLoop.logInToFxA = sandbox.stub();
TestUtils.Simulate.click(
view.getDOMNode().querySelector(".signin-link"));
view.getDOMNode().querySelector(".signin-link a"));
sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
});
});
describe("SettingsDropdown", function() {
var view;
beforeEach(function() {
navigator.mozLoop.logInToFxA = sandbox.stub();
navigator.mozLoop.logOutFromFxA = sandbox.stub();
});
it("should show a signin entry when user is not authenticated",
function() {
navigator.mozLoop.loggedInToFxA = false;
var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
expect(view.getDOMNode().querySelectorAll(".icon-signout"))
.to.have.length.of(0);
expect(view.getDOMNode().querySelectorAll(".icon-signin"))
.to.have.length.of(1);
});
it("should show a signout entry when user is authenticated", function() {
navigator.mozLoop.loggedInToFxA = true;
var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
expect(view.getDOMNode().querySelectorAll(".icon-signout"))
.to.have.length.of(1);
expect(view.getDOMNode().querySelectorAll(".icon-signin"))
.to.have.length.of(0);
});
it("should show an account entry when user is authenticated", function() {
navigator.mozLoop.loggedInToFxA = true;
var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
expect(view.getDOMNode().querySelectorAll(".icon-account"))
.to.have.length.of(1);
});
it("should hide any account entry when user is not authenticated",
function() {
navigator.mozLoop.loggedInToFxA = false;
var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
expect(view.getDOMNode().querySelectorAll(".icon-account"))
.to.have.length.of(0);
});
it("should sign in the user on click when unauthenticated", function() {
navigator.mozLoop.loggedInToFxA = false;
var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
TestUtils.Simulate.click(
view.getDOMNode().querySelector(".icon-signin"));
sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
});
it("should sign out the user on click when authenticated", function() {
navigator.mozLoop.loggedInToFxA = true;
var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
TestUtils.Simulate.click(
view.getDOMNode().querySelector(".icon-signout"));
sinon.assert.calledOnce(navigator.mozLoop.logOutFromFxA);
});
});
describe("#render", function() {
it("should render a ToSView", function() {
TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
@ -264,9 +336,9 @@ describe("loop.panel", function() {
getStrings: function(key) {
var text;
if (key === "share_email_subject2")
if (key === "share_email_subject3")
text = "email-subject";
else if (key === "share_email_body2")
else if (key === "share_email_body3")
text = "{{callUrl}}";
return JSON.stringify({textContent: text});

View File

@ -13,4 +13,5 @@ skip-if = !debug
[browser_mozLoop_prefs.js]
[browser_mozLoop_doNotDisturb.js]
skip-if = buildapp == 'mulet'
[browser_toolbarbutton.js]
[browser_mozLoop_pluralStrings.js]

View File

@ -146,14 +146,14 @@ add_task(function* () {
compareContacts(contacts[i], kContacts[i]);
}
// Add a contact.
info("Add a contact.");
let deferred = Promise.defer();
gExpectedAdds.push(kDanglingContact);
LoopContacts.add(kDanglingContact, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kDanglingContact);
// Check if it's persisted.
info("Check if it's persisted.");
LoopContacts.get(contact._guid, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kDanglingContact);
@ -163,8 +163,8 @@ add_task(function* () {
yield deferred.promise;
});
// Test removing all contacts.
add_task(function* () {
info("Test removing all contacts.");
let contacts = yield promiseLoadContacts();
let deferred = Promise.defer();
@ -183,7 +183,7 @@ add_task(function* () {
add_task(function* () {
let contacts = yield promiseLoadContacts();
// Get a single contact.
info("Get a single contact.");
let deferred = Promise.defer();
LoopContacts.get(contacts[1]._guid, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
@ -192,7 +192,7 @@ add_task(function* () {
});
yield deferred.promise;
// Get a single contact by id.
info("Get a single contact by id.");
let deferred = Promise.defer();
LoopContacts.getByServiceId(2, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
@ -201,7 +201,7 @@ add_task(function* () {
});
yield deferred.promise;
// Get a couple of contacts.
info("Get a couple of contacts.");
let deferred = Promise.defer();
let toRetrieve = [contacts[0], contacts[2], contacts[3]];
LoopContacts.getMany(toRetrieve.map(contact => contact._guid), (err, result) => {
@ -217,7 +217,7 @@ add_task(function* () {
});
yield deferred.promise;
// Get all contacts.
info("Get all contacts.");
deferred = Promise.defer();
LoopContacts.getAll((err, contacts) => {
Assert.ok(!err, "There shouldn't be an error");
@ -228,7 +228,7 @@ add_task(function* () {
});
yield deferred.promise;
// Get a non-existent contact.
info("Get a non-existent contact.");
deferred = Promise.defer();
LoopContacts.get(1000, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
@ -242,7 +242,7 @@ add_task(function* () {
add_task(function* () {
let contacts = yield promiseLoadContacts();
// Remove a single contact.
info("Remove a single contact.");
let deferred = Promise.defer();
let toRemove = contacts[2]._guid;
gExpectedRemovals.push(toRemove);
@ -257,7 +257,7 @@ add_task(function* () {
});
yield deferred.promise;
// Remove a non-existing contact.
info("Remove a non-existing contact.");
deferred = Promise.defer();
LoopContacts.remove(1000, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
@ -266,7 +266,7 @@ add_task(function* () {
});
yield deferred.promise;
// Remove multiple contacts.
info("Remove multiple contacts.");
deferred = Promise.defer();
toRemove = [contacts[0]._guid, contacts[1]._guid];
gExpectedRemovals.push(...toRemove);
@ -292,7 +292,7 @@ add_task(function* () {
const newBday = (new Date(403920000000)).toISOString();
// Update a single contact.
info("Update a single contact.");
let deferred = Promise.defer();
let toUpdate = {
_guid: contacts[2]._guid,
@ -306,7 +306,7 @@ add_task(function* () {
LoopContacts.get(toUpdate._guid, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(contact.bday, newBday, "Birthday should be the same");
// Check that all other properties were left intact.
info("Check that all other properties were left intact.");
contacts[2].bday = newBday;
compareContacts(contact, contacts[2]);
deferred.resolve();
@ -314,7 +314,7 @@ add_task(function* () {
});
yield deferred.promise;
// Update a non-existing contact.
info("Update a non-existing contact.");
deferred = Promise.defer();
toUpdate = {
_guid: 1000,
@ -333,7 +333,7 @@ add_task(function* () {
add_task(function* () {
let contacts = yield promiseLoadContacts();
// Block contact.
info("Block contact.");
let deferred = Promise.defer();
let toBlock = contacts[1]._guid;
gExpectedUpdates.push(toBlock);
@ -344,7 +344,7 @@ add_task(function* () {
LoopContacts.get(toBlock, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
// Check that all other properties were left intact.
info("Check that all other properties were left intact.");
delete contact.blocked;
compareContacts(contact, contacts[1]);
deferred.resolve();
@ -352,7 +352,7 @@ add_task(function* () {
});
yield deferred.promise;
// Block a non-existing contact.
info("Block a non-existing contact.");
deferred = Promise.defer();
LoopContacts.block(1000, err => {
Assert.ok(err, "There should be an error");
@ -362,7 +362,7 @@ add_task(function* () {
});
yield deferred.promise;
// Unblock a contact.
info("Unblock a contact.");
deferred = Promise.defer();
let toUnblock = contacts[1]._guid;
gExpectedUpdates.push(toUnblock);
@ -373,7 +373,7 @@ add_task(function* () {
LoopContacts.get(toUnblock, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
// Check that all other properties were left intact.
info("Check that all other properties were left intact.");
delete contact.blocked;
compareContacts(contact, contacts[1]);
deferred.resolve();
@ -381,7 +381,7 @@ add_task(function* () {
});
yield deferred.promise;
// Unblock a non-existing contact.
info("Unblock a non-existing contact.");
deferred = Promise.defer();
LoopContacts.unblock(1000, err => {
Assert.ok(err, "There should be an error");

View File

@ -0,0 +1,30 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test the toolbar button states.
*/
"use strict";
const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).
MozLoopServiceInternal;
registerCleanupFunction(function*() {
MozLoopService.doNotDisturb = false;
yield MozLoopServiceInternal.clearError("testing");
});
add_task(function* test_doNotDisturb() {
yield MozLoopService.doNotDisturb = true;
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
yield MozLoopService.doNotDisturb = false;
Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is not in disabled state");
});
add_task(function* test_error() {
yield MozLoopServiceInternal.setError("testing", {});
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
yield MozLoopServiceInternal.clearError("testing");
Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is not in error state");
});

View File

@ -34,6 +34,8 @@
return false;
}
function noop(){}
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
@ -43,11 +45,15 @@
);
var mockClient = {
requestCallUrl: function() {}
requestCallUrl: noop,
requestCallUrlInfo: noop
};
var mockConversationModel = new loop.shared.models.ConversationModel({}, {sdk: {}});
// Fake notifier
var mockNotifier = {};
var Example = React.createClass({displayName: 'Example',
render: function() {
var cx = React.addons.classSet;
@ -105,16 +111,17 @@
React.DOM.strong(null, "Note:"), " 332px wide."
),
Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}},
PanelView({callUrl: "http://invalid.example.url/", client: mockClient})
PanelView({client: mockClient, notifier: mockNotifier,
callUrl: "http://invalid.example.url/"})
),
Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient})
PanelView({client: mockClient, notifier: mockNotifier})
)
),
Section({name: "IncomingCallView"},
Example({summary: "Default", dashed: "true", style: {width: "280px"}},
IncomingCallView(null)
IncomingCallView({model: mockConversationModel})
)
),
@ -122,55 +129,81 @@
React.DOM.h3(null, "Desktop Conversation Window"),
React.DOM.div({className: "conversation-window"},
Example({summary: "Default (260x265)", dashed: "true"},
ConversationToolbar({video: {enabled: true}, audio: {enabled: true}})
ConversationToolbar({video: {enabled: true},
audio: {enabled: true},
hangup: noop,
publishStream: noop})
),
Example({summary: "Video muted"},
ConversationToolbar({video: {enabled: false}, audio: {enabled: true}})
ConversationToolbar({video: {enabled: false},
audio: {enabled: true},
hangup: noop,
publishStream: noop})
),
Example({summary: "Audio muted"},
ConversationToolbar({video: {enabled: true}, audio: {enabled: false}})
ConversationToolbar({video: {enabled: true},
audio: {enabled: false},
hangup: noop,
publishStream: noop})
)
),
React.DOM.h3(null, "Standalone"),
React.DOM.div({className: "standalone"},
Example({summary: "Default"},
ConversationToolbar({video: {enabled: true}, audio: {enabled: true}})
ConversationToolbar({video: {enabled: true},
audio: {enabled: true},
hangup: noop,
publishStream: noop})
),
Example({summary: "Video muted"},
ConversationToolbar({video: {enabled: false}, audio: {enabled: true}})
ConversationToolbar({video: {enabled: false},
audio: {enabled: true},
hangup: noop,
publishStream: noop})
),
Example({summary: "Audio muted"},
ConversationToolbar({video: {enabled: true}, audio: {enabled: false}})
ConversationToolbar({video: {enabled: true},
audio: {enabled: false},
hangup: noop,
publishStream: noop})
)
)
),
Section({name: "StartConversationView"},
Example({summary: "Start conversation view", dashed: "true"},
React.DOM.div({className: "standalone"},
StartConversationView({model: mockConversationModel,
client: mockClient})
client: mockClient,
notifier: mockNotifier})
)
)
),
Section({name: "ConversationView"},
Example({summary: "Desktop conversation window", dashed: "true",
style: {width: "260px", height: "265px"}},
React.DOM.div({className: "conversation-window"},
ConversationView({video: {enabled: true}, audio: {enabled: true},
model: mockConversationModel})
ConversationView({sdk: {},
model: mockConversationModel,
video: {enabled: true},
audio: {enabled: true}})
)
),
Example({summary: "Standalone version"},
React.DOM.div({className: "standalone"},
ConversationView({video: {enabled: true}, audio: {enabled: true},
model: mockConversationModel})
ConversationView({sdk: {},
model: mockConversationModel,
video: {enabled: true},
audio: {enabled: true}})
)
),
Example({summary: "Default"},
ConversationView({sdk: {},
model: mockConversationModel,
video: {enabled: true},
audio: {enabled: true}})
)
),
@ -183,10 +216,10 @@
FeedbackView({feedbackApiClient: stageFeedbackApiClient})
),
Example({summary: "Detailed form", dashed: "true", style: {width: "280px"}},
FeedbackView({step: "form"})
FeedbackView({feedbackApiClient: stageFeedbackApiClient, step: "form"})
),
Example({summary: "Thank you!", dashed: "true", style: {width: "280px"}},
FeedbackView({step: "finished"})
FeedbackView({feedbackApiClient: stageFeedbackApiClient, step: "finished"})
)
),

View File

@ -34,6 +34,8 @@
return false;
}
function noop(){}
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
@ -43,11 +45,15 @@
);
var mockClient = {
requestCallUrl: function() {}
requestCallUrl: noop,
requestCallUrlInfo: noop
};
var mockConversationModel = new loop.shared.models.ConversationModel({}, {sdk: {}});
// Fake notifier
var mockNotifier = {};
var Example = React.createClass({
render: function() {
var cx = React.addons.classSet;
@ -105,16 +111,17 @@
<strong>Note:</strong> 332px wide.
</p>
<Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
<PanelView callUrl="http://invalid.example.url/" client={mockClient} />
<PanelView client={mockClient} notifier={mockNotifier}
callUrl="http://invalid.example.url/" />
</Example>
<Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} />
<PanelView client={mockClient} notifier={mockNotifier} />
</Example>
</Section>
<Section name="IncomingCallView">
<Example summary="Default" dashed="true" style={{width: "280px"}}>
<IncomingCallView />
<IncomingCallView model={mockConversationModel} />
</Example>
</Section>
@ -122,56 +129,82 @@
<h3>Desktop Conversation Window</h3>
<div className="conversation-window">
<Example summary="Default (260x265)" dashed="true">
<ConversationToolbar video={{enabled: true}} audio={{enabled: true}} />
<ConversationToolbar video={{enabled: true}}
audio={{enabled: true}}
hangup={noop}
publishStream={noop} />
</Example>
<Example summary="Video muted">
<ConversationToolbar video={{enabled: false}} audio={{enabled: true}} />
<ConversationToolbar video={{enabled: false}}
audio={{enabled: true}}
hangup={noop}
publishStream={noop} />
</Example>
<Example summary="Audio muted">
<ConversationToolbar video={{enabled: true}} audio={{enabled: false}} />
<ConversationToolbar video={{enabled: true}}
audio={{enabled: false}}
hangup={noop}
publishStream={noop} />
</Example>
</div>
<h3>Standalone</h3>
<div className="standalone">
<Example summary="Default">
<ConversationToolbar video={{enabled: true}} audio={{enabled: true}} />
<ConversationToolbar video={{enabled: true}}
audio={{enabled: true}}
hangup={noop}
publishStream={noop} />
</Example>
<Example summary="Video muted">
<ConversationToolbar video={{enabled: false}} audio={{enabled: true}} />
<ConversationToolbar video={{enabled: false}}
audio={{enabled: true}}
hangup={noop}
publishStream={noop} />
</Example>
<Example summary="Audio muted">
<ConversationToolbar video={{enabled: true}} audio={{enabled: false}} />
<ConversationToolbar video={{enabled: true}}
audio={{enabled: false}}
hangup={noop}
publishStream={noop} />
</Example>
</div>
</Section>
<Section name="StartConversationView">
<Example summary="Start conversation view" dashed="true">
<div className="standalone">
<StartConversationView model={mockConversationModel}
client={mockClient} />
client={mockClient}
notifier={mockNotifier} />
</div>
</Example>
</Section>
<Section name="ConversationView">
<Example summary="Desktop conversation window" dashed="true"
style={{width: "260px", height: "265px"}}>
<div className="conversation-window">
<ConversationView video={{enabled: true}} audio={{enabled: true}}
model={mockConversationModel} />
<ConversationView sdk={{}}
model={mockConversationModel}
video={{enabled: true}}
audio={{enabled: true}} />
</div>
</Example>
<Example summary="Standalone version">
<div className="standalone">
<ConversationView video={{enabled: true}} audio={{enabled: true}}
model={mockConversationModel} />
<ConversationView sdk={{}}
model={mockConversationModel}
video={{enabled: true}}
audio={{enabled: true}} />
</div>
</Example>
<Example summary="Default">
<ConversationView sdk={{}}
model={mockConversationModel}
video={{enabled: true}}
audio={{enabled: true}} />
</Example>
</Section>
<Section name="FeedbackView">
@ -183,10 +216,10 @@
<FeedbackView feedbackApiClient={stageFeedbackApiClient} />
</Example>
<Example summary="Detailed form" dashed="true" style={{width: "280px"}}>
<FeedbackView step="form" />
<FeedbackView feedbackApiClient={stageFeedbackApiClient} step="form" />
</Example>
<Example summary="Thank you!" dashed="true" style={{width: "280px"}}>
<FeedbackView step="finished" />
<FeedbackView feedbackApiClient={stageFeedbackApiClient} step="finished" />
</Example>
</Section>

View File

@ -6,45 +6,246 @@
share_link_header_text=Share this link to invite someone to talk:
## LOCALIZATION NOTE(invitee_name_label): Displayed when obtaining a url.
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
## Click the label icon at the end of the url field.
invitee_name_label=Who are you inviting?
## LOCALIZATION NOTE(invitee_expire_days_label): Allows the user to adjust
## the expiry time. Click the label icon at the end of the url field to see where
## this is:
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
## Semicolon-separated list of plural forms. See:
## http://developer.mozilla.org/en/docs/Localization_and_Plurals
## In this item, don't translate the part between {{..}}
invitee_expire_days_label=Invitation will expire in {{expiry_time}} day;Invitation will expire in {{expiry_time}} days
## LOCALIZATION NOTE(invitee_expire_hours_label): Allows the user to adjust
## the expiry time. Click the label icon are the end of the url field to see where
## this is:
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
## Semicolon-separated list of plural forms. See:
## http://developer.mozilla.org/en/docs/Localization_and_Plurals
## In this item, don't translate the part between {{..}}
invitee_expire_hours_label=Invitation will expire in {{expiry_time}} hour;Invitation will expire in {{expiry_time}} hours
# Status text
display_name_guest=Guest
display_name_dnd_status=Do Not Disturb
display_name_available_status=Available
# Error bars
## LOCALIZATION NOTE(unable_retrieve_url,session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
## These may be displayed at the top of the panel here:
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
unable_retrieve_url=Sorry, we were unable to retrieve a call url.
session_expired_error_description=Session expired. All URLs you have previously created and shared will no longer work.
could_not_authenticate=Could Not Authenticate
password_changed_question=Did you change your password?
try_again_later=Please try again later
could_not_connect=Could Not Connect To The Server
check_internet_connection=Please check your internet connection
login_expired=Your Login Has Expired
service_not_available=Service Unavailable At This Time
problem_accessing_account=There Was A Problem Accessing Your Account
## LOCALIZATION NOTE(retry_button): Displayed when there is an error to retry
## the appropriate action.
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error for location
retry_button=Retry
share_email_subject3=You have been invited to a conversation
## LOCALIZATION NOTE (share_email_body3): In this item, don't translate the
## part between {{..}} and leave the \r\n\r\n part alone
share_email_body3=To accept this invitation, just copy or click this link to start your conversation:\r\n\r\n{{callUrl}}
share_button=Email
copy_url_button=Copy
copied_url_button=Copied!
panel_footer_signin_or_signup_link=Sign In or Sign Up
settings_menu_item_account=Account
settings_menu_item_settings=Settings
settings_menu_item_signout=Sign Out
settings_menu_item_signin=Sign In
settings_menu_button_tooltip=Settings
# Contact Strings (Panel)
## LOCALIZATION NOTE(contacts_search_placeholder): This is the placeholder text for
## the search field at https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
contacts_search_placesholder=Search…
## LOCALIZATION NOTE (new_contact_button): This is the button to open the
## new contact sub-panel.
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
## for where this appears on the UI
new_contact_button=New Contact
## LOCALIZATION NOTE (new_contact_name_placeholder, new_contact_email_placeholder):
## These are the placeholders for the fields for entering a new contact
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
## and click the 'New Contact' button to see the fields.
new_contact_name_placeholder=Name
new_contact_email_placeholder=Email
## LOCALIZATION NOTE (add_contact_button):
## This is the button to actually add the new contact
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
## and click the 'New Contact' button to see the fields.
add_contact_button=Add Contact
### LOCALIZATION NOTE (valid_email_text_description): This is displayed when
### the user enters an invalid email address, preventing the addition of the
### contact.
valid_email_text_description=Please enter a valid email address
## LOCALIZATION NOTE (add_or_import_contact_title): This is the subtitle of the panel
## at https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
add_or_import_contact_title=Add or Import Contact
## LOCALIZATION NOTE (import_contacts_button, importing_contacts_progress_button):
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
## for where these appear on the UI
import_contacts_button=Import
importing_contacts_progress_button=Importing…
## LOCALIZATION NOTE(sync_contacts_button): This button is displayed in place of
## importing_contacts_button once contacts have been imported once.
sync_contacts_button=Sync Contacts
## LOCALIZATION NOTE(import_failed_description simple): Displayed when an import of
## contacts fails. This is displayed in the error field here:
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
import_failed_description_simple=Sorry, contact import failed
## LOCALIZATION NOTE(import_failed_description): Displayed when an import of contacts
## fails and the user may need more help. In this item, don't translate the part between
## {{..}} because this will be replaced by the label from import_failed_support_link_label.
## This is displayed in the error field here:
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
import_failed_description=Sorry, contact import failed, please see our {{import_failed_support_link_label}} for help
import_failed_support_link_label=support site
## LOCALIZATION NOTE(remove_contact_menu_button): Displayed in the contact list in
## a pop-up menu next to the contact's name.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
remove_contact_menu_button=Remove Contact
## LOCALIZATION NOTE(confirm_delete_contact_alert): This is an alert that is displayed
## to confirm deletion of a contact.
confirm_delete_contact_alert=Are you sure you want to delete this contact?
## LOCALIZATION NOTE(confirm_delete_contact_remove_button, confirm_delete_contact_cancel_button):
## These are displayed on the alert with confirm_delete_contact_alert
confirm_delete_contact_remove_button=Remove Contact
confirm_delete_contact_cancel_button=Cancel
## LOCALIZATION NOTE(block_contact_menu_button): Displayed in the contact list in
## a pop-up menu next to the contact's name, used to block a contact from calling
## the user. https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
block_contact_menu_button=Block Contact
## LOCALIZATION NOTE(unblock_contact_menu_button): Displayed in the contact list in
## a pop-up menu next to the contact's name, used to unblock a contact and allow them
## to call the user. https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
unblock_contact_menu_button=Unblock Contact
## LOCALIZATION NOTE(edit_contact_menu_button): Displayed in the contact list in a
## pop-up menu next to the contact's name.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
edit_contact_menu_button=Edit Contact…
## LOCALIZATION NOTE(edit_contact_tile): This is the subtitle of the edit contact
## panel. It is displayed when Edit Contact is selected.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
edit_contact_title=Edit Contact
## LOCALIZATION NOTE(edit_contact_name_label, edit_contact_email_label):
## These fields are display when the Edit Contact button is selected.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
edit_contact_name_label=Name
edit_contact_email_label=Email
## LOCALIZATION NOTE(edit_contact_name_label, edit_contact_email_label):
## These button is displayed when the Edit Contact button is selected and is used
## to accept the change.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
edit_contact_done_button=Done
## LOCALIZATION NOTE(audio_call_menu_button): Displayed in the contact list in a
## pop-up menu next to the contact's name.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
audio_call_menu_button=Audio Conversation
## LOCALIZATION NOTE(video_call_menu_button): Displayed in the contact list in a
## pop-up menu next to the contact's name.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
video_call_menu_button=Video Conversation
# Conversation Window Strings
incoming_call_title=Incoming Call…
incoming_call=Incoming call
incoming_call_answer_button=Answer
incoming_call_answer_audio_only_tooltip=Answer with voice
incoming_call_decline_button=Decline
incoming_call_decline_and_block_button=Decline and Block
initiate_call_button_label2=Ready to start your conversation?
incoming_call_title2=Conversation Request
incoming_call_accept_button=Accept
incoming_call_accept_audio_only_tooltip=Accept with voice
incoming_call_cancel_button=Cancel
incoming_call_cancel_and_block_button=Cancel and Block
incoming_call_block_button=Block
hangup_button_title=Hang up
hangup_button_caption=End Call
hangup_button_caption2=Exit
mute_local_audio_button_title=Mute your audio
unmute_local_audio_button_title=Unmute your audio
mute_local_video_button_title=Mute your video
unmute_local_video_button_title=Unmute your video
## LOCALIZATION NOTE (call_with_contact_title): The title displayed
## when calling a contact. Don't translate the part between {{..}} because
## this will be replaced by the contact's name.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#call-outgoing
call_with_contact_title=Conversation with {{contactName}}
# Outgoing conversation
outgoing_call_title=Start conversation?
initiate_audio_video_call_button2=Start
initiate_audio_video_call_tooltip2=Start a video conversation
initiate_audio_call_button2=Voice conversation
initiate_call_cancel_button=Cancel
## LOCALIZATION NOTE (call_progress_connecting_description): This is displayed
## whilst the client is contacting the client at the other end of the connection
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#call-outgoing
call_progress_connecting_description=Connecting…
## LOCALIZATION NOTE (call_progress_ringing_description): This is displayed
## when the other client is actually ringing.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#call-outgoing
call_progress_ringing_description=Ringing…
peer_ended_conversation2=The person you were calling has ended the conversation.
call_has_ended=Your call has ended.
conversation_has_ended=Your conversation has ended.
restart_call=Rejoin
generic_failure_title=Something went wrong.
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
## LOCALIZATION NOTE (contact_offline_title): Title for
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#link-prompt
## displayed when the contact is offline.
contact_offline_title=This person is not online
## LOCALIZATION NOTE (call_timeout_notification_text): Title for
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#link-prompt
## displayed when the call didn't go through.
call_timeout_notification_text=Your call did not go through.
## LOCALIZATION NOTE (retry_call_button, cancel_button, email_link_button):
## These buttons are displayed when a call has failed:
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#link-prompt
retry_call_button=Retry
email_link_button=Email Link
cancel_button=Cancel
cannot_start_call_session_not_ready=Can't start call, session is not ready.
network_disconnected=The network connection terminated abruptly.
connection_error_see_console_notification=Call failed; see console for details.
## LOCALIZATION NOTE (legal_text_and_links2): In this item, don't translate the
## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
## parts between {{..}} because these will be replaced with links with the labels
## from legal_text_tos and legal_text_privacy.
legal_text_and_links2=By using this product you agree to the {{terms_of_use}} \
## from legal_text_tos and legal_text_privacy. clientShortname will be replaced
## by the brand name, or fall back to client_shortname_fallback
legal_text_and_links3=By using {{clientShortname}} you agree to the {{terms_of_use}} \
and {{privacy_notice}}.
legal_text_tos = Terms of Use
legal_text_privacy = Privacy Notice
client_shortname_fallback=this product
feedback_call_experience_heading=How was your call experience?
feedback_call_experience_heading2=How was your conversation?
feedback_what_makes_you_sad=What makes you sad?
feedback_thank_you_heading=Thank you for your feedback!
feedback_category_audio_quality=Audio quality
@ -60,13 +261,10 @@ feedback_back_button=Back
## http://developer.mozilla.org/en/docs/Localization_and_Plurals
## In this item, don't translate the part between {{..}}
feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
share_email_subject2=Invitation to chat
## LOCALIZATION NOTE (share_email_body2): In this item, don't translate the
## part between {{..}} and leave the \r\n\r\n part alone
share_email_body2=Please click this link to call me:\r\n\r\n{{callUrl}}
share_button=Email
copy_url_button=Copy
copied_url_button=Copied!
panel_footer_signin_or_signup_link=Sign In or Sign Up
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User

View File

@ -1322,6 +1322,7 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
list-style-image: url("chrome://browser/skin/loop/toolbar-inverted@2x.png");
}
#loop-call-button[state="disabled"] > .toolbarbutton-badge-container,
#loop-call-button[disabled="true"] > .toolbarbutton-badge-container {
-moz-image-region: rect(0, 72px, 36px, 36px);
}
@ -1357,6 +1358,7 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
-moz-image-region: rect(0, 64px, 64px, 0) !important;
}
#loop-call-button[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
#loop-call-button[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
-moz-image-region: rect(0, 128px, 64px, 64px);
}

View File

@ -161,6 +161,7 @@ toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-c
-moz-image-region: rect(0, 32px, 32px, 0) !important;
}
#loop-call-button[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
#loop-call-button[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
-moz-image-region: rect(0, 64px, 32px, 32px);
}

View File

@ -177,6 +177,7 @@ toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
list-style-image: url(chrome://browser/skin/loop/toolbar-inverted.png);
}
#loop-call-button[state="disabled"] > .toolbarbutton-badge-container,
#loop-call-button[disabled="true"] > .toolbarbutton-badge-container {
-moz-image-region: rect(0, 36px, 18px, 18px);
}

View File

@ -3833,6 +3833,7 @@ MOZ_ANDROID_BEAM=
MOZ_LOCALE_SWITCHER=
MOZ_ANDROID_SEARCH_ACTIVITY=
MOZ_ANDROID_MLS_STUMBLER=
MOZ_ANDROID_SHARE_OVERLAY=
ACCESSIBILITY=1
MOZ_TIME_MANAGER=
MOZ_PAY=
@ -4855,6 +4856,13 @@ if test -n "$MOZ_ANDROID_MLS_STUMBLER"; then
AC_DEFINE(MOZ_ANDROID_MLS_STUMBLER)
fi
dnl ========================================================
dnl = Include share overlay on Android
dnl ========================================================
if test -n "$MOZ_ANDROID_SHARE_OVERLAY"; then
AC_DEFINE(MOZ_ANDROID_SHARE_OVERLAY)
fi
dnl ========================================================
dnl = Enable IPDL's "expensive" unit tests
dnl ========================================================
@ -8447,6 +8455,7 @@ AC_SUBST(MOZ_ANDROID_BEAM)
AC_SUBST(MOZ_LOCALE_SWITCHER)
AC_SUBST(MOZ_DISABLE_GECKOVIEW)
AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY)
AC_SUBST(MOZ_ANDROID_SHARE_OVERLAY)
AC_SUBST(MOZ_ANDROID_MLS_STUMBLER)
AC_SUBST(ENABLE_STRIP)
AC_SUBST(PKG_SKIP_STRIP)

View File

@ -339,6 +339,19 @@ enum BluetoothObjectType {
TYPE_INVALID
};
enum BluetoothA2dpAudioState {
A2DP_AUDIO_STATE_REMOTE_SUSPEND,
A2DP_AUDIO_STATE_STOPPED,
A2DP_AUDIO_STATE_STARTED,
};
enum BluetoothA2dpConnectionState {
A2DP_CONNECTION_STATE_DISCONNECTED,
A2DP_CONNECTION_STATE_CONNECTING,
A2DP_CONNECTION_STATE_CONNECTED,
A2DP_CONNECTION_STATE_DISCONNECTING
};
enum ControlPlayStatus {
PLAYSTATUS_STOPPED = 0x00,
PLAYSTATUS_PLAYING = 0x01,
@ -349,6 +362,16 @@ enum ControlPlayStatus {
PLAYSTATUS_ERROR = 0xFF,
};
enum BluetoothAvrcpMediaAttribute {
AVRCP_MEDIA_ATTRIBUTE_TITLE,
AVRCP_MEDIA_ATTRIBUTE_ARTIST,
AVRCP_MEDIA_ATTRIBUTE_ALBUM,
AVRCP_MEDIA_ATTRIBUTE_TRACK_NUM,
AVRCP_MEDIA_ATTRIBUTE_NUM_TRACKS,
AVRCP_MEDIA_ATTRIBUTE_GENRE,
AVRCP_MEDIA_ATTRIBUTE_PLAYING_TIME
};
enum BluetoothAvrcpPlayerAttribute {
AVRCP_PLAYER_ATTRIBUTE_EQUALIZER,
AVRCP_PLAYER_ATTRIBUTE_REPEAT,
@ -378,6 +401,13 @@ enum BluetoothAvrcpNotification {
AVRCP_NTF_CHANGED
};
enum BluetoothAvrcpRemoteFeature {
AVRCP_REMOTE_FEATURE_NONE,
AVRCP_REMOTE_FEATURE_METADATA,
AVRCP_REMOTE_FEATURE_ABSOLUTE_VOLUME,
AVRCP_REMOTE_FEATURE_BROWSE
};
struct BluetoothAvrcpElementAttribute {
uint32_t mId;
nsString mValue;
@ -392,6 +422,12 @@ struct BluetoothAvrcpNotificationParam {
uint8_t mValues[256];
};
struct BluetoothAvrcpPlayerSettings {
uint8_t mNumAttr;
uint8_t mIds[256];
uint8_t mValues[256];
};
END_BLUETOOTH_NAMESPACE
#endif // mozilla_dom_bluetooth_bluetoothcommon_h__

View File

@ -7,7 +7,6 @@
#include "base/basictypes.h"
#include "BluetoothA2dpManager.h"
#include "BluetoothInterface.h"
#include "BluetoothCommon.h"
#include "BluetoothService.h"
#include "BluetoothSocket.h"
@ -37,171 +36,42 @@ namespace {
#endif
} // anonymous namespace
class SinkPropertyChangedHandler : public nsRunnable
{
public:
SinkPropertyChangedHandler(const BluetoothSignal& aSignal)
: mSignal(aSignal)
{
}
NS_IMETHOD
Run()
{
MOZ_ASSERT(NS_IsMainThread());
BluetoothA2dpManager* a2dp = BluetoothA2dpManager::Get();
NS_ENSURE_TRUE(a2dp, NS_ERROR_FAILURE);
a2dp->HandleSinkPropertyChanged(mSignal);
return NS_OK;
}
private:
BluetoothSignal mSignal;
};
class RequestPlayStatusTask : public nsRunnable
{
public:
RequestPlayStatusTask()
{
MOZ_ASSERT(!NS_IsMainThread());
}
nsresult Run()
{
MOZ_ASSERT(NS_IsMainThread());
BluetoothSignal signal(NS_LITERAL_STRING(REQUEST_MEDIA_PLAYSTATUS_ID),
NS_LITERAL_STRING(KEY_ADAPTER),
InfallibleTArray<BluetoothNamedValue>());
BluetoothService* bs = BluetoothService::Get();
NS_ENSURE_TRUE(bs, NS_ERROR_FAILURE);
bs->DistributeSignal(signal);
return NS_OK;
}
};
#if ANDROID_VERSION > 17
class UpdateRegisterNotificationTask : public nsRunnable
{
public:
UpdateRegisterNotificationTask(BluetoothAvrcpEvent aEvent, uint32_t aParam)
: mEvent(aEvent)
, mParam(aParam)
{
MOZ_ASSERT(!NS_IsMainThread());
}
nsresult Run()
{
MOZ_ASSERT(NS_IsMainThread());
BluetoothA2dpManager* a2dp = BluetoothA2dpManager::Get();
NS_ENSURE_TRUE(a2dp, NS_OK);
a2dp->UpdateRegisterNotification(mEvent, mParam);
return NS_OK;
}
private:
BluetoothAvrcpEvent mEvent;
uint32_t mParam;
};
/*
* This function maps attribute id and returns corresponding values
* Attribute id refers to btrc_media_attr_t in bt_rc.h
*/
static void
ConvertAttributeString(int aAttrId, nsAString& aAttrStr)
ConvertAttributeString(BluetoothAvrcpMediaAttribute aAttrId,
nsAString& aAttrStr)
{
BluetoothA2dpManager* a2dp = BluetoothA2dpManager::Get();
NS_ENSURE_TRUE_VOID(a2dp);
switch (aAttrId) {
case BTRC_MEDIA_ATTR_TITLE:
case AVRCP_MEDIA_ATTRIBUTE_TITLE:
a2dp->GetTitle(aAttrStr);
break;
case BTRC_MEDIA_ATTR_ARTIST:
case AVRCP_MEDIA_ATTRIBUTE_ARTIST:
a2dp->GetArtist(aAttrStr);
break;
case BTRC_MEDIA_ATTR_ALBUM:
case AVRCP_MEDIA_ATTRIBUTE_ALBUM:
a2dp->GetAlbum(aAttrStr);
break;
case BTRC_MEDIA_ATTR_TRACK_NUM:
case AVRCP_MEDIA_ATTRIBUTE_TRACK_NUM:
aAttrStr.AppendInt(a2dp->GetMediaNumber());
break;
case BTRC_MEDIA_ATTR_NUM_TRACKS:
case AVRCP_MEDIA_ATTRIBUTE_NUM_TRACKS:
aAttrStr.AppendInt(a2dp->GetTotalMediaNumber());
break;
case BTRC_MEDIA_ATTR_GENRE:
case AVRCP_MEDIA_ATTRIBUTE_GENRE:
// TODO: we currently don't support genre from music player
aAttrStr.Truncate();
break;
case BTRC_MEDIA_ATTR_PLAYING_TIME:
case AVRCP_MEDIA_ATTRIBUTE_PLAYING_TIME:
aAttrStr.AppendInt(a2dp->GetDuration());
break;
}
}
class UpdateElementAttrsTask : public nsRunnable
{
public:
UpdateElementAttrsTask(uint8_t aNumAttr, const btrc_media_attr_t* aPlayerAttrs)
: mNumAttr(aNumAttr)
{
MOZ_ASSERT(!NS_IsMainThread());
mAttrs = new BluetoothAvrcpElementAttribute[mNumAttr];
for (uint8_t i = 0; i < mNumAttr; ++i) {
mAttrs[i].mId = aPlayerAttrs[i];
}
}
nsresult Run()
{
MOZ_ASSERT(NS_IsMainThread());
for (uint8_t i = 0; i < mNumAttr; ++i) {
ConvertAttributeString(mAttrs[i].mId, mAttrs[i].mValue);
}
NS_ENSURE_TRUE(sBtAvrcpInterface, NS_OK);
sBtAvrcpInterface->GetElementAttrRsp(mNumAttr, mAttrs, nullptr);
return NS_OK;
}
private:
uint8_t mNumAttr;
nsAutoArrayPtr<BluetoothAvrcpElementAttribute> mAttrs;
};
class UpdatePassthroughCmdTask : public nsRunnable
{
public:
UpdatePassthroughCmdTask(const nsAString& aName)
: mName(aName)
{
MOZ_ASSERT(!NS_IsMainThread());
}
nsresult Run()
{
MOZ_ASSERT(NS_IsMainThread());
NS_NAMED_LITERAL_STRING(type, "media-button");
BroadcastSystemMessage(type, BluetoothValue(mName));
return NS_OK;
}
private:
nsString mName;
};
#endif
NS_IMETHODIMP
@ -233,288 +103,21 @@ BluetoothA2dpManager::Reset()
}
static void
AvStatusToSinkString(btav_connection_state_t aStatus, nsAString& aState)
AvStatusToSinkString(BluetoothA2dpConnectionState aState, nsAString& aString)
{
nsAutoString state;
if (aStatus == BTAV_CONNECTION_STATE_DISCONNECTED) {
aState = NS_LITERAL_STRING("disconnected");
} else if (aStatus == BTAV_CONNECTION_STATE_CONNECTING) {
aState = NS_LITERAL_STRING("connecting");
} else if (aStatus == BTAV_CONNECTION_STATE_CONNECTED) {
aState = NS_LITERAL_STRING("connected");
} else if (aStatus == BTAV_CONNECTION_STATE_DISCONNECTING) {
aState = NS_LITERAL_STRING("disconnecting");
} else {
BT_WARNING("Unknown sink state");
static const nsLiteralString sString[] = {
[A2DP_CONNECTION_STATE_DISCONNECTED] = NS_LITERAL_STRING("disconnected"),
[A2DP_CONNECTION_STATE_CONNECTING] = NS_LITERAL_STRING("connecting"),
[A2DP_CONNECTION_STATE_CONNECTED] = NS_LITERAL_STRING("connected"),
[A2DP_CONNECTION_STATE_DISCONNECTING] = NS_LITERAL_STRING("disconnecting")
};
if (aState >= MOZ_ARRAY_LENGTH(sString)) {
BT_WARNING("Unknown sink state %d", static_cast<int>(aState));
return;
}
aString = sString[aState];
}
static void
A2dpConnectionStateCallback(btav_connection_state_t aState,
bt_bdaddr_t* aBdAddress)
{
MOZ_ASSERT(!NS_IsMainThread());
nsString remoteDeviceBdAddress;
BdAddressTypeToString(aBdAddress, remoteDeviceBdAddress);
nsString a2dpState;
AvStatusToSinkString(aState, a2dpState);
InfallibleTArray<BluetoothNamedValue> props;
BT_APPEND_NAMED_VALUE(props, "State", a2dpState);
BluetoothSignal signal(NS_LITERAL_STRING("AudioSink"),
remoteDeviceBdAddress, props);
NS_DispatchToMainThread(new SinkPropertyChangedHandler(signal));
}
static void
A2dpAudioStateCallback(btav_audio_state_t aState,
bt_bdaddr_t* aBdAddress)
{
MOZ_ASSERT(!NS_IsMainThread());
nsString remoteDeviceBdAddress;
BdAddressTypeToString(aBdAddress, remoteDeviceBdAddress);
nsString a2dpState;
if (aState == BTAV_AUDIO_STATE_STARTED) {
a2dpState = NS_LITERAL_STRING("playing");
} else if (aState == BTAV_AUDIO_STATE_STOPPED) {
// for avdtp state stop stream
a2dpState = NS_LITERAL_STRING("connected");
} else if (aState == BTAV_AUDIO_STATE_REMOTE_SUSPEND) {
// for avdtp state suspend stream from remote side
a2dpState = NS_LITERAL_STRING("connected");
}
InfallibleTArray<BluetoothNamedValue> props;
BT_APPEND_NAMED_VALUE(props, "State", a2dpState);
BluetoothSignal signal(NS_LITERAL_STRING("AudioSink"),
remoteDeviceBdAddress, props);
NS_DispatchToMainThread(new SinkPropertyChangedHandler(signal));
}
#if ANDROID_VERSION > 17
/*
* Avrcp 1.3 callbacks
*/
/*
* This function is to request Gaia player application to update
* current play status.
* Callback for play status request
*/
static void
AvrcpGetPlayStatusCallback()
{
MOZ_ASSERT(!NS_IsMainThread());
NS_DispatchToMainThread(new RequestPlayStatusTask());
}
/*
* This function is trying to get element attributes, which request from CT
* Unlike BlueZ only calls UpdateMetaData, bluedroid does not cache meta data
* information, but instead uses callback AvrcpGetElementAttrCallback and
* call get_element_attr_rsp() to reply request.
*
* Callback to fetch the get element attributes of the current song
* aNumAttr: It represents the number of attributes requested in aPlayerAttrs
* aPlayerAttrs: It represents Attribute Ids
*/
static void
AvrcpGetElementAttrCallback(uint8_t aNumAttr, btrc_media_attr_t* aPlayerAttrs)
{
MOZ_ASSERT(!NS_IsMainThread());
NS_DispatchToMainThread(new UpdateElementAttrsTask(aNumAttr, aPlayerAttrs));
}
/*
* Callback for register notification (Play state change/track change/...)
* To reply RegisterNotification INTERIM response
* See AVRCP 1.3 Spec 25.2
* aParam: It only valids if event_id is BTRC_EVT_PLAY_POS_CHANGED,
* which is playback interval time
*/
static void
AvrcpRegisterNotificationCallback(btrc_event_id_t aEventId, uint32_t aParam)
{
BluetoothAvrcpEvent event;
MOZ_ASSERT(!NS_IsMainThread());
switch (aEventId) {
case BTRC_EVT_PLAY_STATUS_CHANGED:
event = AVRCP_EVENT_PLAY_STATUS_CHANGED;
case BTRC_EVT_TRACK_CHANGE:
event = AVRCP_EVENT_TRACK_CHANGE;
case BTRC_EVT_TRACK_REACHED_END:
event = AVRCP_EVENT_TRACK_REACHED_END;
case BTRC_EVT_TRACK_REACHED_START:
event = AVRCP_EVENT_TRACK_REACHED_START;
case BTRC_EVT_PLAY_POS_CHANGED:
event = AVRCP_EVENT_PLAY_POS_CHANGED;
case BTRC_EVT_APP_SETTINGS_CHANGED:
event = AVRCP_EVENT_APP_SETTINGS_CHANGED;
break;
default:
BT_LOGR("Unknown event 0x%x", aEventId);
return;
}
NS_DispatchToMainThread(new UpdateRegisterNotificationTask(event, aParam));
}
/*
* Player application settings is optional for Avrcp 1.3
* B2G 1.3 currently does not support Player application setting
* related functions. Support Player Setting in the future version
*/
static void
AvrcpListPlayerAppAttributeCallback()
{
MOZ_ASSERT(!NS_IsMainThread());
// TODO: Support avrcp application setting related functions
}
static void
AvrcpListPlayerAppValuesCallback(btrc_player_attr_t aAttrId)
{
MOZ_ASSERT(!NS_IsMainThread());
// TODO: Support avrcp application setting related functions
}
static void
AvrcpGetPlayerAppValueCallback(uint8_t aNumAttr,
btrc_player_attr_t* aPlayerAttrs)
{
MOZ_ASSERT(!NS_IsMainThread());
// TODO: Support avrcp application setting related functions
}
static void
AvrcpGetPlayerAppAttrsTextCallback(uint8_t aNumAttr,
btrc_player_attr_t* PlayerAttrs)
{
MOZ_ASSERT(!NS_IsMainThread());
// TODO: Support avrcp application setting related functions
}
static void
AvrcpGetPlayerAppValuesTextCallback(uint8_t aAttrId, uint8_t aNumVal,
uint8_t* PlayerVals)
{
MOZ_ASSERT(!NS_IsMainThread());
// TODO: Support avrcp application setting related functions
}
static void
AvrcpSetPlayerAppValueCallback(btrc_player_settings_t* aPlayerVals)
{
MOZ_ASSERT(!NS_IsMainThread());
// TODO: Support avrcp application setting related functions
}
#endif
#if ANDROID_VERSION > 18
/*
* This callback function is to get CT features from Feature Bit Mask.
* If Advanced Control Player bit is set, CT supports
* volume sync (absolute volume feature). If Browsing bit is set, Avrcp 1.4
* Browse feature will be supported
*/
static void
AvrcpRemoteFeaturesCallback(bt_bdaddr_t* aBdAddress,
btrc_remote_features_t aFeatures)
{
// TODO: Support avrcp 1.4 absolute volume/browse
}
/*
* This callback function is to get notification that volume changed on the
* remote car kit (if it supports Avrcp 1.4), not notification from phone.
*/
static void
AvrcpRemoteVolumeChangedCallback(uint8_t aVolume, uint8_t aCType)
{
// TODO: Support avrcp 1.4 absolute volume/browse
}
/*
* This callback function is to handle passthrough commands.
*/
static void
AvrcpPassThroughCallback(int aId, int aKeyState)
{
// Fast-forward and rewind key events won't be generated from bluedroid
// stack after ANDROID_VERSION > 18, but via passthrough callback.
nsAutoString name;
NS_ENSURE_TRUE_VOID(aKeyState == AVRC_KEY_PRESS_STATE ||
aKeyState == AVRC_KEY_RELEASE_STATE);
switch (aId) {
case AVRC_ID_FAST_FOR:
if (aKeyState == AVRC_KEY_PRESS_STATE) {
name.AssignLiteral("media-fast-forward-button-press");
} else {
name.AssignLiteral("media-fast-forward-button-release");
}
break;
case AVRC_ID_REWIND:
if (aKeyState == AVRC_KEY_PRESS_STATE) {
name.AssignLiteral("media-rewind-button-press");
} else {
name.AssignLiteral("media-rewind-button-release");
}
break;
default:
BT_WARNING("Unable to handle the unknown PassThrough command %d", aId);
break;
}
if (!name.IsEmpty()) {
NS_DispatchToMainThread(new UpdatePassthroughCmdTask(name));
}
}
#endif
static btav_callbacks_t sBtA2dpCallbacks = {
sizeof(sBtA2dpCallbacks),
A2dpConnectionStateCallback,
A2dpAudioStateCallback
};
#if ANDROID_VERSION > 17
static btrc_callbacks_t sBtAvrcpCallbacks = {
sizeof(sBtAvrcpCallbacks),
#if ANDROID_VERSION > 18
AvrcpRemoteFeaturesCallback,
#endif
AvrcpGetPlayStatusCallback,
AvrcpListPlayerAppAttributeCallback,
AvrcpListPlayerAppValuesCallback,
AvrcpGetPlayerAppValueCallback,
AvrcpGetPlayerAppAttrsTextCallback,
AvrcpGetPlayerAppValuesTextCallback,
AvrcpSetPlayerAppValueCallback,
AvrcpGetElementAttrCallback,
AvrcpRegisterNotificationCallback,
#if ANDROID_VERSION > 18
AvrcpRemoteVolumeChangedCallback,
AvrcpPassThroughCallback
#endif
};
#endif
#if ANDROID_VERSION > 17
class InitAvrcpResultHandler MOZ_FINAL : public BluetoothAvrcpResultHandler
{
@ -570,8 +173,8 @@ public:
sBtAvrcpInterface = btInf->GetBluetoothAvrcpInterface();
NS_ENSURE_TRUE_VOID(sBtAvrcpInterface);
sBtAvrcpInterface->Init(&sBtAvrcpCallbacks,
new InitAvrcpResultHandler(mRes));
BluetoothA2dpManager* a2dpManager = BluetoothA2dpManager::Get();
sBtAvrcpInterface->Init(a2dpManager, new InitAvrcpResultHandler(mRes));
#else
/* ...or signal success otherwise. */
if (mRes) {
@ -600,7 +203,8 @@ BluetoothA2dpManager::InitA2dpInterface(BluetoothProfileResultHandler* aRes)
sBtA2dpInterface = btInf->GetBluetoothA2dpInterface();
NS_ENSURE_TRUE_VOID(sBtA2dpInterface);
sBtA2dpInterface->Init(&sBtA2dpCallbacks, new InitA2dpResultHandler(aRes));
BluetoothA2dpManager* a2dpManager = BluetoothA2dpManager::Get();
sBtA2dpInterface->Init(a2dpManager, new InitA2dpResultHandler(aRes));
}
BluetoothA2dpManager::~BluetoothA2dpManager()
@ -1305,5 +909,230 @@ BluetoothA2dpManager::GetArtist(nsAString& aArtist)
aArtist.Assign(mArtist);
}
/*
* A2DP Notifications
*/
void
BluetoothA2dpManager::ConnectionStateNotification(BluetoothA2dpConnectionState aState,
const nsAString& aBdAddr)
{
MOZ_ASSERT(NS_IsMainThread());
nsString a2dpState;
AvStatusToSinkString(aState, a2dpState);
InfallibleTArray<BluetoothNamedValue> props;
BT_APPEND_NAMED_VALUE(props, "State", a2dpState);
HandleSinkPropertyChanged(BluetoothSignal(NS_LITERAL_STRING("AudioSink"),
nsString(aBdAddr), props));
}
void
BluetoothA2dpManager::AudioStateNotification(BluetoothA2dpAudioState aState,
const nsAString& aBdAddr)
{
MOZ_ASSERT(NS_IsMainThread());
nsString a2dpState;
if (aState == A2DP_AUDIO_STATE_STARTED) {
a2dpState = NS_LITERAL_STRING("playing");
} else if (aState == A2DP_AUDIO_STATE_STOPPED) {
// for avdtp state stop stream
a2dpState = NS_LITERAL_STRING("connected");
} else if (aState == A2DP_AUDIO_STATE_REMOTE_SUSPEND) {
// for avdtp state suspend stream from remote side
a2dpState = NS_LITERAL_STRING("connected");
}
InfallibleTArray<BluetoothNamedValue> props;
BT_APPEND_NAMED_VALUE(props, "State", a2dpState);
HandleSinkPropertyChanged(BluetoothSignal(NS_LITERAL_STRING("AudioSink"),
nsString(aBdAddr), props));
}
/*
* AVRCP Notifications
*/
void
BluetoothA2dpManager::GetPlayStatusNotification()
{
MOZ_ASSERT(NS_IsMainThread());
BluetoothService* bs = BluetoothService::Get();
if (!bs) {
return;
}
bs->DistributeSignal(
BluetoothSignal(NS_LITERAL_STRING(REQUEST_MEDIA_PLAYSTATUS_ID),
NS_LITERAL_STRING(KEY_ADAPTER),
InfallibleTArray<BluetoothNamedValue>()));
}
/* Player application settings is optional for AVRCP 1.3. B2G
* currently does not support player-application-setting related
* functionality.
*/
void
BluetoothA2dpManager::ListPlayerAppAttrNotification()
{
MOZ_ASSERT(NS_IsMainThread());
// TODO: Support AVRCP application-setting-related functions
}
void
BluetoothA2dpManager::ListPlayerAppValuesNotification(
BluetoothAvrcpPlayerAttribute aAttrId)
{
MOZ_ASSERT(NS_IsMainThread());
// TODO: Support AVRCP application-setting-related functions
}
void
BluetoothA2dpManager::GetPlayerAppValueNotification(
uint8_t aNumAttrs, const BluetoothAvrcpPlayerAttribute* aAttrs)
{
MOZ_ASSERT(NS_IsMainThread());
// TODO: Support AVRCP application-setting-related functions
}
void
BluetoothA2dpManager::GetPlayerAppAttrsTextNotification(
uint8_t aNumAttrs, const BluetoothAvrcpPlayerAttribute* aAttrs)
{
MOZ_ASSERT(NS_IsMainThread());
// TODO: Support AVRCP application-setting-related functions
}
void
BluetoothA2dpManager::GetPlayerAppValuesTextNotification(
uint8_t aAttrId, uint8_t aNumVals, const uint8_t* aValues)
{
MOZ_ASSERT(NS_IsMainThread());
// TODO: Support AVRCP application-setting-related functions
}
void
BluetoothA2dpManager::SetPlayerAppValueNotification(
const BluetoothAvrcpPlayerSettings& aSettings)
{
MOZ_ASSERT(NS_IsMainThread());
// TODO: Support AVRCP application-setting-related functions
}
/* This method returns element attributes, which are requested from
* CT. Unlike BlueZ it calls only UpdateMetaData. Bluedroid does not cache
* meta-data information, but instead uses |GetElementAttrNotifications|
* and |GetElementAttrRsp| request them.
*/
void
BluetoothA2dpManager::GetElementAttrNotification(
uint8_t aNumAttrs, const BluetoothAvrcpMediaAttribute* aAttrs)
{
MOZ_ASSERT(NS_IsMainThread());
nsAutoArrayPtr<BluetoothAvrcpElementAttribute> attrs(
new BluetoothAvrcpElementAttribute[aNumAttrs]);
for (uint8_t i = 0; i < aNumAttrs; ++i) {
attrs[i].mId = aAttrs[i];
ConvertAttributeString(
static_cast<BluetoothAvrcpMediaAttribute>(attrs[i].mId),
attrs[i].mValue);
}
#if ANDROID_VERSION >= 18
MOZ_ASSERT(sBtAvrcpInterface);
sBtAvrcpInterface->GetElementAttrRsp(aNumAttrs, attrs, nullptr);
#endif // ANDROID_VERSION >= 18
}
void
BluetoothA2dpManager::RegisterNotificationNotification(
BluetoothAvrcpEvent aEvent, uint32_t aParam)
{
MOZ_ASSERT(NS_IsMainThread());
BluetoothA2dpManager* a2dp = BluetoothA2dpManager::Get();
if (!a2dp) {
return;
}
#if ANDROID_VERSION >= 18
a2dp->UpdateRegisterNotification(aEvent, aParam);
#endif // ANDROID_VERSION >= 18
}
/* This method is used to get CT features from the Feature Bit Mask. If
* Advanced Control Player bit is set, the CT supports volume sync (absolute
* volume feature). If Browsing bit is set, AVRCP 1.4 Browse feature will be
* supported.
*/
void
BluetoothA2dpManager::RemoteFeatureNotification(
const nsAString& aBdAddr, unsigned long aFeatures)
{
MOZ_ASSERT(NS_IsMainThread());
// TODO: Support AVRCP 1.4 absolute volume/browse
}
/* This method is used to get notifications about volume changes on the
* remote car kit (if it supports AVRCP 1.4), not notification from phone.
*/
void
BluetoothA2dpManager::VolumeChangeNotification(uint8_t aVolume,
uint8_t aCType)
{
MOZ_ASSERT(NS_IsMainThread());
// TODO: Support AVRCP 1.4 absolute volume/browse
}
void
BluetoothA2dpManager::PassthroughCmdNotification(int aId, int aKeyState)
{
MOZ_ASSERT(NS_IsMainThread());
// Fast-forward and rewind key events won't be generated from bluedroid
// stack after ANDROID_VERSION > 18, but via passthrough callback.
nsAutoString name;
NS_ENSURE_TRUE_VOID(aKeyState == AVRC_KEY_PRESS_STATE ||
aKeyState == AVRC_KEY_RELEASE_STATE);
switch (aId) {
case AVRC_ID_FAST_FOR:
if (aKeyState == AVRC_KEY_PRESS_STATE) {
name.AssignLiteral("media-fast-forward-button-press");
} else {
name.AssignLiteral("media-fast-forward-button-release");
}
break;
case AVRC_ID_REWIND:
if (aKeyState == AVRC_KEY_PRESS_STATE) {
name.AssignLiteral("media-rewind-button-press");
} else {
name.AssignLiteral("media-rewind-button-release");
}
break;
default:
BT_WARNING("Unable to handle the unknown PassThrough command %d", aId);
return;
}
NS_NAMED_LITERAL_STRING(type, "media-button");
BroadcastSystemMessage(type, BluetoothValue(name));
}
NS_IMPL_ISUPPORTS(BluetoothA2dpManager, nsIObserver)

View File

@ -8,11 +8,14 @@
#define mozilla_dom_bluetooth_bluetootha2dpmanager_h__
#include "BluetoothCommon.h"
#include "BluetoothInterface.h"
#include "BluetoothProfileController.h"
#include "BluetoothProfileManagerBase.h"
BEGIN_BLUETOOTH_NAMESPACE
class BluetoothA2dpManager : public BluetoothProfileManagerBase
, public BluetoothA2dpNotificationHandler
, public BluetoothAvrcpNotificationHandler
{
public:
BT_DECL_PROFILE_MGR_BASE
@ -63,7 +66,6 @@ public:
void GetArtist(nsAString& aArtist);
private:
class SinkPropertyChangedHandler;
BluetoothA2dpManager();
void ResetA2dp();
void ResetAvrcp();
@ -71,6 +73,46 @@ private:
void HandleShutdown();
void NotifyConnectionStatusChanged();
void ConnectionStateNotification(BluetoothA2dpConnectionState aState,
const nsAString& aBdAddr) MOZ_OVERRIDE;
void AudioStateNotification(BluetoothA2dpAudioState aState,
const nsAString& aBdAddr) MOZ_OVERRIDE;
void GetPlayStatusNotification() MOZ_OVERRIDE;
void ListPlayerAppAttrNotification() MOZ_OVERRIDE;
void ListPlayerAppValuesNotification(
BluetoothAvrcpPlayerAttribute aAttrId) MOZ_OVERRIDE;
void GetPlayerAppValueNotification(
uint8_t aNumAttrs,
const BluetoothAvrcpPlayerAttribute* aAttrs) MOZ_OVERRIDE;
void GetPlayerAppAttrsTextNotification(
uint8_t aNumAttrs,
const BluetoothAvrcpPlayerAttribute* aAttrs) MOZ_OVERRIDE;
void GetPlayerAppValuesTextNotification(
uint8_t aAttrId, uint8_t aNumVals, const uint8_t* aValues) MOZ_OVERRIDE;
void SetPlayerAppValueNotification(
const BluetoothAvrcpPlayerSettings& aSettings) MOZ_OVERRIDE;
void GetElementAttrNotification(
uint8_t aNumAttrs,
const BluetoothAvrcpMediaAttribute* aAttrs) MOZ_OVERRIDE;
void RegisterNotificationNotification(
BluetoothAvrcpEvent aEvent, uint32_t aParam) MOZ_OVERRIDE;
void RemoteFeatureNotification(
const nsAString& aBdAddr, unsigned long aFeatures) MOZ_OVERRIDE;
void VolumeChangeNotification(uint8_t aVolume, uint8_t aCType) MOZ_OVERRIDE;
void PassthroughCmdNotification(int aId, int aKeyState) MOZ_OVERRIDE;
nsString mDeviceAddress;
nsRefPtr<BluetoothProfileController> mController;

View File

@ -436,18 +436,6 @@ Convert(bt_device_type_t aIn, BluetoothDeviceType& aOut)
return NS_OK;
}
#if ANDROID_VERSION >= 18
static nsresult
Convert(const bt_remote_version_t& aIn, BluetoothRemoteInfo& aOut)
{
aOut.mVerMajor = aIn.version;
aOut.mVerMinor = aIn.sub_ver;
aOut.mManufacturer = aIn.manufacturer;
return NS_OK;
}
#endif
static nsresult
Convert(const bt_service_record_t& aIn, BluetoothServiceRecord& aOut)
{
@ -715,7 +703,52 @@ Convert(bthf_volume_type_t aIn, BluetoothHandsfreeVolumeType& aOut)
return NS_OK;
}
static nsresult
Convert(btav_connection_state_t aIn, BluetoothA2dpConnectionState& aOut)
{
static const BluetoothA2dpConnectionState sConnectionState[] = {
CONVERT(BTAV_CONNECTION_STATE_DISCONNECTED,
A2DP_CONNECTION_STATE_DISCONNECTED),
CONVERT(BTAV_CONNECTION_STATE_CONNECTING,
A2DP_CONNECTION_STATE_CONNECTING),
CONVERT(BTAV_CONNECTION_STATE_CONNECTED,
A2DP_CONNECTION_STATE_CONNECTED),
CONVERT(BTAV_CONNECTION_STATE_DISCONNECTING,
A2DP_CONNECTION_STATE_DISCONNECTING),
};
if (aIn >= MOZ_ARRAY_LENGTH(sConnectionState)) {
return NS_ERROR_ILLEGAL_VALUE;
}
aOut = sConnectionState[aIn];
return NS_OK;
}
static nsresult
Convert(btav_audio_state_t aIn, BluetoothA2dpAudioState& aOut)
{
static const BluetoothA2dpAudioState sAudioState[] = {
CONVERT(BTAV_AUDIO_STATE_REMOTE_SUSPEND, A2DP_AUDIO_STATE_REMOTE_SUSPEND),
CONVERT(BTAV_AUDIO_STATE_STOPPED, A2DP_AUDIO_STATE_STOPPED),
CONVERT(BTAV_AUDIO_STATE_STARTED, A2DP_AUDIO_STATE_STARTED),
};
if (aIn >= MOZ_ARRAY_LENGTH(sAudioState)) {
return NS_ERROR_ILLEGAL_VALUE;
}
aOut = sAudioState[aIn];
return NS_OK;
}
#if ANDROID_VERSION >= 18
static nsresult
Convert(const bt_remote_version_t& aIn, BluetoothRemoteInfo& aOut)
{
aOut.mVerMajor = aIn.version;
aOut.mVerMinor = aIn.sub_ver;
aOut.mManufacturer = aIn.manufacturer;
return NS_OK;
}
static nsresult
Convert(ControlPlayStatus aIn, btrc_play_status_t& aOut)
{
@ -749,6 +782,23 @@ Convert(enum BluetoothAvrcpPlayerAttribute aIn, btrc_player_attr_t& aOut)
return NS_OK;
}
static nsresult
Convert(btrc_player_attr_t aIn, enum BluetoothAvrcpPlayerAttribute& aOut)
{
static const BluetoothAvrcpPlayerAttribute sPlayerAttr[] = {
CONVERT(0, static_cast<BluetoothAvrcpPlayerAttribute>(0)), // invalid, [0] required by gcc
CONVERT(BTRC_PLAYER_ATTR_EQUALIZER, AVRCP_PLAYER_ATTRIBUTE_EQUALIZER),
CONVERT(BTRC_PLAYER_ATTR_REPEAT, AVRCP_PLAYER_ATTRIBUTE_REPEAT),
CONVERT(BTRC_PLAYER_ATTR_SHUFFLE, AVRCP_PLAYER_ATTRIBUTE_SHUFFLE),
CONVERT(BTRC_PLAYER_ATTR_SCAN, AVRCP_PLAYER_ATTRIBUTE_SCAN)
};
if (!aIn || aIn >= MOZ_ARRAY_LENGTH(sPlayerAttr)) {
return NS_ERROR_ILLEGAL_VALUE;
}
aOut = sPlayerAttr[aIn];
return NS_OK;
}
static nsresult
Convert(enum BluetoothAvrcpStatus aIn, btrc_status_t& aOut)
{
@ -784,6 +834,47 @@ Convert(enum BluetoothAvrcpEvent aIn, btrc_event_id_t& aOut)
return NS_OK;
}
static nsresult
Convert(btrc_event_id_t aIn, enum BluetoothAvrcpEvent& aOut)
{
static const BluetoothAvrcpEvent sEventId[] = {
CONVERT(0, static_cast<BluetoothAvrcpEvent>(0)), // invalid, [0] required by gcc
CONVERT(BTRC_EVT_PLAY_STATUS_CHANGED, AVRCP_EVENT_PLAY_STATUS_CHANGED),
CONVERT(BTRC_EVT_TRACK_CHANGE, AVRCP_EVENT_TRACK_CHANGE),
CONVERT(BTRC_EVT_TRACK_REACHED_END, AVRCP_EVENT_TRACK_REACHED_END),
CONVERT(BTRC_EVT_TRACK_REACHED_START, AVRCP_EVENT_TRACK_REACHED_START),
CONVERT(BTRC_EVT_PLAY_POS_CHANGED, AVRCP_EVENT_PLAY_POS_CHANGED),
CONVERT(6, static_cast<BluetoothAvrcpEvent>(0)), // invalid, [6] required by gcc
CONVERT(7, static_cast<BluetoothAvrcpEvent>(0)), // invalid, [7] required by gcc
CONVERT(BTRC_EVT_APP_SETTINGS_CHANGED, AVRCP_EVENT_APP_SETTINGS_CHANGED)
};
if (!aIn || aIn >= MOZ_ARRAY_LENGTH(sEventId)) {
return NS_ERROR_ILLEGAL_VALUE;
}
aOut = sEventId[aIn];
return NS_OK;
}
static nsresult
Convert(btrc_media_attr_t aIn, enum BluetoothAvrcpMediaAttribute& aOut)
{
static const BluetoothAvrcpMediaAttribute sEventId[] = {
CONVERT(0, static_cast<BluetoothAvrcpMediaAttribute>(0)), // invalid, [0] required by gcc
CONVERT(BTRC_MEDIA_ATTR_TITLE, AVRCP_MEDIA_ATTRIBUTE_TITLE),
CONVERT(BTRC_MEDIA_ATTR_ARTIST, AVRCP_MEDIA_ATTRIBUTE_ARTIST),
CONVERT(BTRC_MEDIA_ATTR_ALBUM, AVRCP_MEDIA_ATTRIBUTE_ALBUM),
CONVERT(BTRC_MEDIA_ATTR_TRACK_NUM, AVRCP_MEDIA_ATTRIBUTE_TRACK_NUM),
CONVERT(BTRC_MEDIA_ATTR_NUM_TRACKS, AVRCP_MEDIA_ATTRIBUTE_NUM_TRACKS),
CONVERT(BTRC_MEDIA_ATTR_GENRE, AVRCP_MEDIA_ATTRIBUTE_GENRE),
CONVERT(BTRC_MEDIA_ATTR_PLAYING_TIME, AVRCP_MEDIA_ATTRIBUTE_PLAYING_TIME)
};
if (!aIn || aIn >= MOZ_ARRAY_LENGTH(sEventId)) {
return NS_ERROR_ILLEGAL_VALUE;
}
aOut = sEventId[aIn];
return NS_OK;
}
static nsresult
Convert(enum BluetoothAvrcpNotification aIn, btrc_notification_type_t& aOut)
{
@ -811,7 +902,28 @@ Convert(const BluetoothAvrcpElementAttribute& aIn, btrc_element_attr_val_t& aOut
return NS_OK;
}
#endif
static nsresult
Convert(const btrc_player_settings_t& aIn, BluetoothAvrcpPlayerSettings& aOut)
{
aOut.mNumAttr = aIn.num_attr;
memcpy(aOut.mIds, aIn.attr_ids, aIn.num_attr);
memcpy(aOut.mValues, aIn.attr_values, aIn.num_attr);
return NS_OK;
}
#endif // ANDROID_VERSION >= 18
#if ANDROID_VERSION >= 19
static nsresult
Convert(btrc_remote_features_t aIn, unsigned long& aOut)
{
/* The input type's name is misleading. The converted value is
* actually a bitmask.
*/
aOut = static_cast<unsigned long>(aIn);
return NS_OK;
}
#endif // ANDROID_VERSION >= 19
/* |ConvertArray| is a helper for converting arrays. Pass an
* instance of this structure as the first argument to |Convert|
@ -2716,6 +2828,69 @@ DispatchBluetoothA2dpResult(
return rv;
}
// Notification handling
//
BluetoothA2dpNotificationHandler::~BluetoothA2dpNotificationHandler()
{ }
static BluetoothA2dpNotificationHandler* sA2dpNotificationHandler;
struct BluetoothA2dpCallback
{
class A2dpNotificationHandlerWrapper
{
public:
typedef BluetoothA2dpNotificationHandler ObjectType;
static ObjectType* GetInstance()
{
MOZ_ASSERT(NS_IsMainThread());
return sA2dpNotificationHandler;
}
};
// Notifications
typedef BluetoothNotificationRunnable2<A2dpNotificationHandlerWrapper,
void,
BluetoothA2dpConnectionState,
nsString,
BluetoothA2dpConnectionState,
const nsAString&>
ConnectionStateNotification;
typedef BluetoothNotificationRunnable2<A2dpNotificationHandlerWrapper,
void,
BluetoothA2dpAudioState,
nsString,
BluetoothA2dpAudioState,
const nsAString&>
AudioStateNotification;
// Bluedroid A2DP callbacks
static void
ConnectionState(btav_connection_state_t aState, bt_bdaddr_t* aBdAddr)
{
ConnectionStateNotification::Dispatch(
&BluetoothA2dpNotificationHandler::ConnectionStateNotification,
aState, aBdAddr);
}
static void
AudioState(btav_audio_state_t aState, bt_bdaddr_t* aBdAddr)
{
AudioStateNotification::Dispatch(
&BluetoothA2dpNotificationHandler::AudioStateNotification,
aState, aBdAddr);
}
};
// Interface
//
BluetoothA2dpInterface::BluetoothA2dpInterface(
const btav_interface_t* aInterface)
: mInterface(aInterface)
@ -2727,10 +2902,19 @@ BluetoothA2dpInterface::~BluetoothA2dpInterface()
{ }
void
BluetoothA2dpInterface::Init(btav_callbacks_t* aCallbacks,
BluetoothA2dpResultHandler* aRes)
BluetoothA2dpInterface::Init(
BluetoothA2dpNotificationHandler* aNotificationHandler,
BluetoothA2dpResultHandler* aRes)
{
bt_status_t status = mInterface->init(aCallbacks);
static btav_callbacks_t sCallbacks = {
sizeof(sCallbacks),
BluetoothA2dpCallback::ConnectionState,
BluetoothA2dpCallback::AudioState
};
sA2dpNotificationHandler = aNotificationHandler;
bt_status_t status = mInterface->init(&sCallbacks);
if (aRes) {
DispatchBluetoothA2dpResult(aRes, &BluetoothA2dpResultHandler::Init,
@ -2834,6 +3018,199 @@ DispatchBluetoothAvrcpResult(
}
return rv;
}
#endif
// Notification handling
//
BluetoothAvrcpNotificationHandler::~BluetoothAvrcpNotificationHandler()
{ }
#if ANDROID_VERSION >= 18
static BluetoothAvrcpNotificationHandler* sAvrcpNotificationHandler;
struct BluetoothAvrcpCallback
{
class AvrcpNotificationHandlerWrapper
{
public:
typedef BluetoothAvrcpNotificationHandler ObjectType;
static ObjectType* GetInstance()
{
MOZ_ASSERT(NS_IsMainThread());
return sAvrcpNotificationHandler;
}
};
// Notifications
typedef BluetoothNotificationRunnable0<AvrcpNotificationHandlerWrapper,
void>
GetPlayStatusNotification;
typedef BluetoothNotificationRunnable0<AvrcpNotificationHandlerWrapper,
void>
ListPlayerAppAttrNotification;
typedef BluetoothNotificationRunnable1<AvrcpNotificationHandlerWrapper,
void,
BluetoothAvrcpPlayerAttribute>
ListPlayerAppValuesNotification;
typedef BluetoothNotificationRunnable2<AvrcpNotificationHandlerWrapper, void,
uint8_t, nsAutoArrayPtr<BluetoothAvrcpPlayerAttribute>,
uint8_t, const BluetoothAvrcpPlayerAttribute*>
GetPlayerAppValueNotification;
typedef BluetoothNotificationRunnable2<AvrcpNotificationHandlerWrapper, void,
uint8_t, nsAutoArrayPtr<BluetoothAvrcpPlayerAttribute>,
uint8_t, const BluetoothAvrcpPlayerAttribute*>
GetPlayerAppAttrsTextNotification;
typedef BluetoothNotificationRunnable3<AvrcpNotificationHandlerWrapper,
void,
uint8_t, uint8_t,
nsAutoArrayPtr<uint8_t>,
uint8_t, uint8_t, const uint8_t*>
GetPlayerAppValuesTextNotification;
typedef BluetoothNotificationRunnable1<AvrcpNotificationHandlerWrapper,
void,
BluetoothAvrcpPlayerSettings,
const BluetoothAvrcpPlayerSettings&>
SetPlayerAppValueNotification;
typedef BluetoothNotificationRunnable2<AvrcpNotificationHandlerWrapper, void,
uint8_t, nsAutoArrayPtr<BluetoothAvrcpMediaAttribute>,
uint8_t, const BluetoothAvrcpMediaAttribute*>
GetElementAttrNotification;
typedef BluetoothNotificationRunnable2<AvrcpNotificationHandlerWrapper,
void,
BluetoothAvrcpEvent, uint32_t>
RegisterNotificationNotification;
#if ANDROID_VERSION >= 19
typedef BluetoothNotificationRunnable2<AvrcpNotificationHandlerWrapper,
void,
nsString, unsigned long,
const nsAString&>
RemoteFeatureNotification;
typedef BluetoothNotificationRunnable2<AvrcpNotificationHandlerWrapper,
void,
uint8_t, uint8_t>
VolumeChangeNotification;
typedef BluetoothNotificationRunnable2<AvrcpNotificationHandlerWrapper,
void,
int, int>
PassthroughCmdNotification;
#endif // ANDROID_VERSION >= 19
// Bluedroid AVRCP callbacks
static void
GetPlayStatus()
{
GetPlayStatusNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::GetPlayStatusNotification);
}
static void
ListPlayerAppAttr()
{
ListPlayerAppAttrNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::ListPlayerAppAttrNotification);
}
static void
ListPlayerAppValues(btrc_player_attr_t aAttrId)
{
ListPlayerAppValuesNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::ListPlayerAppValuesNotification,
aAttrId);
}
static void
GetPlayerAppValue(uint8_t aNumAttrs, btrc_player_attr_t* aAttrs)
{
GetPlayerAppValueNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::GetPlayerAppValueNotification,
aNumAttrs, ConvertArray<btrc_player_attr_t>(aAttrs, aNumAttrs));
}
static void
GetPlayerAppAttrsText(uint8_t aNumAttrs, btrc_player_attr_t* aAttrs)
{
GetPlayerAppAttrsTextNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::GetPlayerAppAttrsTextNotification,
aNumAttrs, ConvertArray<btrc_player_attr_t>(aAttrs, aNumAttrs));
}
static void
GetPlayerAppValuesText(uint8_t aAttrId, uint8_t aNumVals, uint8_t* aVals)
{
GetPlayerAppValuesTextNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::GetPlayerAppValuesTextNotification,
aAttrId, aNumVals, ConvertArray<uint8_t>(aVals, aNumVals));
}
static void
SetPlayerAppValue(btrc_player_settings_t* aVals)
{
SetPlayerAppValueNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::SetPlayerAppValueNotification,
*aVals);
}
static void
GetElementAttr(uint8_t aNumAttrs, btrc_media_attr_t* aAttrs)
{
GetElementAttrNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::GetElementAttrNotification,
aNumAttrs, ConvertArray<btrc_media_attr_t>(aAttrs, aNumAttrs));
}
static void
RegisterNotification(btrc_event_id_t aEvent, uint32_t aParam)
{
RegisterNotificationNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::RegisterNotificationNotification,
aEvent, aParam);
}
#if ANDROID_VERSION >= 19
static void
RemoteFeature(bt_bdaddr_t* aBdAddr, btrc_remote_features_t aFeatures)
{
RemoteFeatureNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::RemoteFeatureNotification,
aBdAddr, aFeatures);
}
static void
VolumeChange(uint8_t aVolume, uint8_t aCType)
{
VolumeChangeNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::VolumeChangeNotification,
aVolume, aCType);
}
static void
PassthroughCmd(int aId, int aKeyState)
{
PassthroughCmdNotification::Dispatch(
&BluetoothAvrcpNotificationHandler::PassthroughCmdNotification,
aId, aKeyState);
}
#endif // ANDROID_VERSION >= 19
};
// Interface
//
BluetoothAvrcpInterface::BluetoothAvrcpInterface(
const btrc_interface_t* aInterface)
@ -2846,10 +3223,34 @@ BluetoothAvrcpInterface::~BluetoothAvrcpInterface()
{ }
void
BluetoothAvrcpInterface::Init(btrc_callbacks_t* aCallbacks,
BluetoothAvrcpResultHandler* aRes)
BluetoothAvrcpInterface::Init(
BluetoothAvrcpNotificationHandler* aNotificationHandler,
BluetoothAvrcpResultHandler* aRes)
{
bt_status_t status = mInterface->init(aCallbacks);
static btrc_callbacks_t sCallbacks = {
sizeof(sCallbacks),
#if ANDROID_VERSION >= 19
BluetoothAvrcpCallback::RemoteFeature,
#endif
BluetoothAvrcpCallback::GetPlayStatus,
BluetoothAvrcpCallback::ListPlayerAppAttr,
BluetoothAvrcpCallback::ListPlayerAppValues,
BluetoothAvrcpCallback::GetPlayerAppValue,
BluetoothAvrcpCallback::GetPlayerAppAttrsText,
BluetoothAvrcpCallback::GetPlayerAppValuesText,
BluetoothAvrcpCallback::SetPlayerAppValue,
BluetoothAvrcpCallback::GetElementAttr,
BluetoothAvrcpCallback::RegisterNotification
#if ANDROID_VERSION >= 19
,
BluetoothAvrcpCallback::VolumeChange,
BluetoothAvrcpCallback::PassthroughCmd
#endif
};
sAvrcpNotificationHandler = aNotificationHandler;
bt_status_t status = mInterface->init(&sCallbacks);
if (aRes) {
DispatchBluetoothAvrcpResult(aRes, &BluetoothAvrcpResultHandler::Init,

View File

@ -265,6 +265,26 @@ private:
// Bluetooth Advanced Audio Interface
//
class BluetoothA2dpNotificationHandler
{
public:
virtual ~BluetoothA2dpNotificationHandler();
virtual void
ConnectionStateNotification(BluetoothA2dpConnectionState aState,
const nsAString& aBdAddr)
{ }
virtual void
AudioStateNotification(BluetoothA2dpAudioState aState,
const nsAString& aBdAddr)
{ }
protected:
BluetoothA2dpNotificationHandler()
{ }
};
class BluetoothA2dpResultHandler
{
public:
@ -288,7 +308,7 @@ class BluetoothA2dpInterface
public:
friend class BluetoothInterface;
void Init(btav_callbacks_t *aCallbacks,
void Init(BluetoothA2dpNotificationHandler* aNotificationHandler,
BluetoothA2dpResultHandler* aRes);
void Cleanup(BluetoothA2dpResultHandler* aRes);
@ -309,6 +329,69 @@ private:
// Bluetooth AVRCP Interface
//
class BluetoothAvrcpNotificationHandler
{
public:
virtual ~BluetoothAvrcpNotificationHandler();
virtual void
GetPlayStatusNotification()
{ }
virtual void
ListPlayerAppAttrNotification()
{ }
virtual void
ListPlayerAppValuesNotification(BluetoothAvrcpPlayerAttribute aAttrId)
{ }
virtual void
GetPlayerAppValueNotification(uint8_t aNumAttrs,
const BluetoothAvrcpPlayerAttribute* aAttrs)
{ }
virtual void
GetPlayerAppAttrsTextNotification(uint8_t aNumAttrs,
const BluetoothAvrcpPlayerAttribute* aAttrs)
{ }
virtual void
GetPlayerAppValuesTextNotification(uint8_t aAttrId, uint8_t aNumVals,
const uint8_t* aValues)
{ }
virtual void
SetPlayerAppValueNotification(const BluetoothAvrcpPlayerSettings& aSettings)
{ }
virtual void
GetElementAttrNotification(uint8_t aNumAttrs,
const BluetoothAvrcpMediaAttribute* aAttrs)
{ }
virtual void
RegisterNotificationNotification(BluetoothAvrcpEvent aEvent,
uint32_t aParam)
{ }
virtual void
RemoteFeatureNotification(const nsAString& aBdAddr, unsigned long aFeatures)
{ }
virtual void
VolumeChangeNotification(uint8_t aVolume, uint8_t aCType)
{ }
virtual void
PassthroughCmdNotification(int aId, int aKeyState)
{ }
protected:
BluetoothAvrcpNotificationHandler()
{ }
};
class BluetoothAvrcpResultHandler
{
public:
@ -348,7 +431,7 @@ class BluetoothAvrcpInterface
public:
friend class BluetoothInterface;
void Init(btrc_callbacks_t* aCallbacks,
void Init(BluetoothAvrcpNotificationHandler* aNotificationHandler,
BluetoothAvrcpResultHandler* aRes);
void Cleanup(BluetoothAvrcpResultHandler* aRes);

View File

@ -22,19 +22,6 @@
BEGIN_BLUETOOTH_NAMESPACE
void
BdAddressTypeToString(bt_bdaddr_t* aBdAddressType, nsAString& aRetBdAddress)
{
uint8_t* addr = aBdAddressType->address;
char bdstr[18];
sprintf(bdstr, "%02x:%02x:%02x:%02x:%02x:%02x",
(int)addr[0],(int)addr[1],(int)addr[2],
(int)addr[3],(int)addr[4],(int)addr[5]);
aRetBdAddress = NS_ConvertUTF8toUTF16(bdstr);
}
uint16_t
UuidToServiceClassInt(const BluetoothUuid& mUuid)
{

View File

@ -7,8 +7,6 @@
#ifndef mozilla_dom_bluetooth_bluetoothutils_h__
#define mozilla_dom_bluetooth_bluetoothutils_h__
#include <hardware/bluetooth.h>
#include "BluetoothCommon.h"
#include "js/TypeDecls.h"
@ -18,10 +16,6 @@ class BluetoothNamedValue;
class BluetoothValue;
class BluetoothReplyRunnable;
void
BdAddressTypeToString(bt_bdaddr_t* aBdAddressType,
nsAString& aRetBdAddress);
uint16_t
UuidToServiceClassInt(const BluetoothUuid& mUuid);

View File

@ -2383,9 +2383,15 @@ RilObject.prototype = {
let request = options.attach ? RIL_REQUEST_GPRS_ATTACH :
RIL_REQUEST_GPRS_DETACH;
this.context.Buf.simpleRequest(request, options);
return;
} else if (RILQUIRKS_SUBSCRIPTION_CONTROL && options.attach) {
this.context.Buf.simpleRequest(REQUEST_SET_DATA_SUBSCRIPTION, options);
return;
}
// We don't really send a request to rild, so instantly reply success to
// RadioInterfaceLayer.
this.sendChromeMessage(options);
},
/**

View File

@ -371,6 +371,25 @@
</provider>
#ifdef MOZ_ANDROID_SHARE_OVERLAY
<!-- Share overlay activity -->
<activity android:name="org.mozilla.gecko.overlays.ui.ShareDialog"
android:label="@string/overlay_share_header"
android:theme="@style/ShareOverlayActivity"
android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<!-- Service to handle requests from overlays. -->
<service android:name="org.mozilla.gecko.overlays.service.OverlayActionService" />
#endif
<!--
Ensure that passwords provider runs in its own process. (Bug 718760.)
Process name is per-application to avoid loading CPs from multiple

View File

@ -90,6 +90,23 @@
to display browser chrome. -->
<!ENTITY locale_system_default "System default">
<!-- Localization note (overlay_share_bookmark_btn_label) : This string is
used in the share overlay menu to select an action. It is the verb
"to bookmark", not the noun "a bookmark". -->
<!ENTITY overlay_share_bookmark_btn_label "Bookmark">
<!ENTITY overlay_share_reading_list_btn_label "Add to Reading List">
<!ENTITY overlay_share_header "Send to &brandShortName;">
<!ENTITY overlay_share_send_other "Send to other devices">
<!-- Localization note (overlay_share_send_tab_btn_label) : Used on the
share overlay menu to represent the "Send Tab" action when the user
either has not set up Sync, or has no other devices to send a tab
to. -->
<!ENTITY overlay_share_send_tab_btn_label "Send to another device">
<!ENTITY overlay_share_no_url "No link found in this share">
<!ENTITY overlay_share_retry "Try again">
<!ENTITY overlay_share_select_device "Select device">
<!ENTITY pref_category_search3 "Search">
<!ENTITY pref_category_search_summary "Customize your search providers">
<!ENTITY pref_category_display "Display">

View File

@ -6,3 +6,6 @@
if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
DEFINES['MOZ_ANDROID_SEARCH_ACTIVITY'] = 1
if CONFIG['MOZ_ANDROID_SHARE_OVERLAY']:
DEFINES['MOZ_ANDROID_SHARE_OVERLAY'] = 1

View File

@ -479,6 +479,22 @@ if CONFIG['MOZ_CRASHREPORTER']:
gbjar.sources += [ 'CrashReporter.java' ]
ANDROID_RES_DIRS += [ SRCDIR + '/crashreporter/res' ]
if CONFIG['MOZ_ANDROID_SHARE_OVERLAY']:
gbjar.sources += [
'overlays/OverlayConstants.java',
'overlays/service/OverlayActionService.java',
'overlays/service/sharemethods/AddBookmark.java',
'overlays/service/sharemethods/AddToReadingList.java',
'overlays/service/sharemethods/ParcelableClientRecord.java',
'overlays/service/sharemethods/SendTab.java',
'overlays/service/sharemethods/ShareMethod.java',
'overlays/ui/OverlayToastHelper.java',
'overlays/ui/SendTabDeviceListArrayAdapter.java',
'overlays/ui/SendTabList.java',
'overlays/ui/SendTabTargetSelectedListener.java',
'overlays/ui/ShareDialog.java',
]
gbjar.sources += sync_java_files
gbjar.generated_sources += sync_generated_java_files
gbjar.extra_jars = [
@ -587,7 +603,8 @@ ANDROID_GENERATED_RESFILES += [
]
for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZILLA_OFFICIAL', 'MOZ_DEBUG',
'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER'):
'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER',
'MOZ_ANDROID_SHARE_OVERLAY'):
if CONFIG[var]:
DEFINES[var] = 1

View File

@ -0,0 +1,68 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays;
/**
* Constants used by the share handler service (and clients).
* The intent API used by the service is defined herein.
*/
public class OverlayConstants {
/*
* OverlayIntentHandler service intent actions.
*/
/*
* Causes the service to broadcast an intent containing state necessary for proper display of
* a UI to select a target share method.
*
* Intent parameters:
*
* None.
*/
public static final String ACTION_PREPARE_SHARE = "org.mozilla.gecko.overlays.ACTION_PREPARE_SHARE";
/*
* Action for sharing a page.
*
* Intent parameters:
*
* $EXTRA_URL: URL of page to share. (required)
* $EXTRA_SHARE_METHOD: Method(s) via which to share this url/title combination. Can be either a
* ShareType or a ShareType[]
* $EXTRA_TITLE: Title of page to share (optional)
* $EXTRA_PARAMETERS: Parcelable of extra data to pass to the ShareMethod (optional)
*/
public static final String ACTION_SHARE = "org.mozilla.gecko.overlays.ACTION_SHARE";
/*
* OverlayIntentHandler service intent extra field keys.
*/
// The URL/title of the page being shared
public static final String EXTRA_URL = "URL";
public static final String EXTRA_TITLE = "TITLE";
// The optional extra Parcelable parameters for a ShareMethod.
public static final String EXTRA_PARAMETERS = "EXTRA";
// The extra field key used for holding the ShareMethod.Type we wish to use for an operation.
public static final String EXTRA_SHARE_METHOD = "SHARE_METHOD";
/*
* ShareMethod UI event intent constants. Broadcast by ShareMethods using LocalBroadcastManager
* when state has changed that requires an update of any currently-displayed share UI.
*/
/*
* Action for a ShareMethod UI event.
*
* Intent parameters:
*
* $EXTRA_SHARE_METHOD: The ShareType to which this event relates.
* ... ShareType-specific parameters as desired... (optional)
*/
public static final String SHARE_METHOD_UI_EVENT = "org.mozilla.gecko.overlays.ACTION_SHARE_METHOD_UI_EVENT";
}

View File

@ -0,0 +1,148 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.service;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable;
import android.util.Log;
import android.view.View;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.overlays.OverlayConstants;
import org.mozilla.gecko.overlays.service.sharemethods.AddBookmark;
import org.mozilla.gecko.overlays.service.sharemethods.AddToReadingList;
import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
import org.mozilla.gecko.overlays.ui.OverlayToastHelper;
import org.mozilla.gecko.util.ThreadUtils;
import java.util.EnumMap;
import java.util.Map;
import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_PREPARE_SHARE;
import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_SHARE;
import static org.mozilla.gecko.overlays.OverlayConstants.EXTRA_SHARE_METHOD;
/**
* A service to receive requests from overlays to perform actions.
* See OverlayConstants for details of the intent API supported by this service.
*
* Currently supported operations are:
*
* Add bookmark*
* Add to reading list*
* Send tab (delegates to Sync's existing handler)
* Future: Load page in background.
*
* * Neither of these incur a page fetch on the service... yet. That will require headless Gecko,
* something we're yet to have. Refactoring Gecko as a service itself and restructing the rest of
* the app to talk to it seems like the way to go there.
*/
public class OverlayActionService extends Service {
private static final String LOGTAG = "GeckoOverlayService";
// Map used for selecting the appropriate helper object when handling a share.
private final Map<ShareMethod.Type, ShareMethod> shareTypes = new EnumMap<>(ShareMethod.Type.class);
// Map relating Strings representing share types to the corresponding ShareMethods.
// Share methods are initialised (and shown in the UI) in the order they are given here.
// This map is used to look up the appropriate ShareMethod when handling a request, as well as
// for identifying which ShareMethod needs re-initialising in response to such an intent (which
// will be necessary in situations such as the deletion of Sync accounts).
// Not a bindable service.
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
return START_NOT_STICKY;
}
// Dispatch intent to appropriate method according to its action.
String action = intent.getAction();
switch (action) {
case ACTION_SHARE:
handleShare(intent);
break;
case ACTION_PREPARE_SHARE:
initShareMethods(getApplicationContext());
break;
default:
throw new IllegalArgumentException("Unsupported intent action: " + action);
}
return START_NOT_STICKY;
}
/**
* Reinitialise all ShareMethods, causing them to broadcast any UI update events necessary.
*/
private void initShareMethods(Context context) {
shareTypes.clear();
shareTypes.put(ShareMethod.Type.ADD_BOOKMARK, new AddBookmark(context));
shareTypes.put(ShareMethod.Type.ADD_TO_READING_LIST, new AddToReadingList(context));
shareTypes.put(ShareMethod.Type.SEND_TAB, new SendTab(context));
}
public void handleShare(final Intent intent) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
Bundle extras = intent.getExtras();
// Fish the parameters out of the Intent.
final String url = extras.getString(OverlayConstants.EXTRA_URL);
final String title = extras.getString(OverlayConstants.EXTRA_TITLE);
final Parcelable extra = extras.getParcelable(OverlayConstants.EXTRA_PARAMETERS);
if (url == null) {
Log.e(LOGTAG, "Null url passed to handleShare!");
return;
}
ShareMethod.Type shareMethodType = (ShareMethod.Type) extras.get(EXTRA_SHARE_METHOD);
ShareMethod shareMethod = shareTypes.get(shareMethodType);
final ShareMethod.Result result = shareMethod.handle(title, url, extra);
// Dispatch the share to the targeted ShareMethod.
switch (result) {
case SUCCESS:
// \o/
OverlayToastHelper.showSuccessToast(getApplicationContext(), shareMethod.getSuccessMesssage());
break;
case TRANSIENT_FAILURE:
// An OnClickListener to do this share again.
View.OnClickListener retryListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
handleShare(intent);
}
};
// Show a failure toast with a retry button.
OverlayToastHelper.showFailureToast(getApplicationContext(), shareMethod.getFailureMessage(), retryListener);
break;
case PERMANENT_FAILURE:
// Show a failure toast without a retry button.
OverlayToastHelper.showFailureToast(getApplicationContext(), shareMethod.getFailureMessage());
break;
default:
Assert.isTrue(false, "Unknown share method result code: " + result);
break;
}
}
});
}
}

View File

@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.service.sharemethods;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Parcelable;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.LocalBrowserDB;
public class AddBookmark extends ShareMethod {
private static final String LOGTAG = "GeckoAddBookmark";
@Override
public Result handle(String title, String url, Parcelable unused) {
ContentResolver resolver = context.getContentResolver();
LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
browserDB.addBookmark(resolver, url, title);
return Result.SUCCESS;
}
public String getSuccessMesssage() {
return context.getResources().getString(R.string.bookmark_added);
}
// Unused.
@Override
public String getFailureMessage() {
return null;
}
public AddBookmark(Context context) {
super(context);
}
}

View File

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.service.sharemethods;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.os.Parcelable;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.LocalBrowserDB;
import static org.mozilla.gecko.db.BrowserContract.Bookmarks;
/**
* ShareMethod to add a page to the reading list.
*
* Inserts the given URL/title pair into the reading list database.
* TODO: In the event the page turns out not to be reader-mode-compatible, freezes sometimes occur
* when we subsequently load the page in reader mode. (Bug 1044781)
*/
public class AddToReadingList extends ShareMethod {
private static final String LOGTAG = "GeckoAddToReadingList";
@Override
public Result handle(String title, String url, Parcelable unused) {
ContentResolver resolver = context.getContentResolver();
LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
ContentValues values = new ContentValues();
values.put(Bookmarks.TITLE, title);
values.put(Bookmarks.URL, url);
browserDB.addReadingListItem(resolver, values);
return Result.SUCCESS;
}
@Override
public String getSuccessMesssage() {
return context.getResources().getString(R.string.reading_list_added);
}
// Unused.
@Override
public String getFailureMessage() {
return null;
}
public AddToReadingList(Context context) {
super(context);
}
}

View File

@ -0,0 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.service.sharemethods;
import android.os.Parcel;
import android.os.Parcelable;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
/**
* An immutable representation of a Sync ClientRecord for Parceling, storing only name, guid, type.
* Implemented this way instead of by making ClientRecord itself parcelable to avoid an undesirable
* dependency between Sync and the IPC system used by the share system (things which really should
* be kept as independent as possible).
*/
public class ParcelableClientRecord implements Parcelable {
private static final String LOGTAG = "GeckoParcelableClientRecord";
public final String name;
public final String type;
public final String guid;
private ParcelableClientRecord(String aName, String aType, String aGUID) {
name = aName;
type = aType;
guid = aGUID;
}
/**
* Create a ParcelableClientRecord from a vanilla ClientRecord.
*/
public static ParcelableClientRecord fromClientRecord(ClientRecord record) {
return new ParcelableClientRecord(record.name, record.type, record.guid);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeString(type);
parcel.writeString(guid);
}
public static final Creator<ParcelableClientRecord> CREATOR = new Creator<ParcelableClientRecord>() {
@Override
public ParcelableClientRecord createFromParcel(final Parcel source) {
String name = source.readString();
String type = source.readString();
String guid = source.readString();
return new ParcelableClientRecord(name, type, guid);
}
@Override
public ParcelableClientRecord[] newArray(final int size) {
return new ParcelableClientRecord[size];
}
};
/**
* Used by SendTabDeviceListArrayAdapter to populate ListViews.
*/
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,370 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.service.sharemethods;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.mozilla.gecko.R;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.overlays.OverlayConstants;
import org.mozilla.gecko.sync.CommandProcessor;
import org.mozilla.gecko.sync.CommandRunner;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.SyncConstants;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* ShareMethod implementation to handle Sync's "Send tab to device" mechanism.
* See OverlayConstants for documentation of OverlayIntentHandler service intent API (which is how
* this class is chiefly interacted with).
*/
public class SendTab extends ShareMethod {
private static final String LOGTAG = "GeckoSendTab";
// Key used in the extras Bundle in the share intent used for a send tab ShareMethod.
public static final String SEND_TAB_TARGET_DEVICES = "SEND_TAB_TARGET_DEVICES";
// Key used in broadcast intent from SendTab ShareMethod specifying available ClientRecords.
public static final String EXTRA_CLIENT_RECORDS = "RECORDS";
// The intent we should dispatch when the button for this ShareMethod is tapped, instead of
// taking the normal action (eg. "Set up sync!")
public static final String OVERRIDE_INTENT = "OVERRIDE_INTENT";
private Set<String> validGUIDs;
// A TabSender appropriate to the account type we're connected to.
private TabSender tabSender;
@Override
public Result handle(String title, String url, Parcelable extra) {
if (extra == null) {
Log.e(LOGTAG, "No target devices specified!");
// Retrying with an identical lack of devices ain't gonna fix it...
return Result.PERMANENT_FAILURE;
}
String[] targetGUIDs = ((Bundle) extra).getStringArray(SEND_TAB_TARGET_DEVICES);
// Ensure all target GUIDs are devices we actually know about.
if (!validGUIDs.containsAll(Arrays.asList(targetGUIDs))) {
// Find the set of invalid GUIDs to provide a nice error message.
Log.e(LOGTAG, "Not all provided GUIDs are real devices:");
for (String targetGUID : targetGUIDs) {
if (!validGUIDs.contains(targetGUID)) {
Log.e(LOGTAG, "Invalid GUID: " + targetGUID);
}
}
return Result.PERMANENT_FAILURE;
}
Log.i(LOGTAG, "Send tab handler invoked.");
final CommandProcessor processor = CommandProcessor.getProcessor();
final String accountGUID = tabSender.getAccountGUID();
Log.d(LOGTAG, "Retrieved local account GUID '" + accountGUID + "'.");
if (accountGUID == null) {
Log.e(LOGTAG, "Cannot determine account GUID");
// It's not completely out of the question that a background sync might come along and
// fix everything for us...
return Result.TRANSIENT_FAILURE;
}
// Queue up the share commands for each destination device.
// Remember that ShareMethod.handle is always run on the background thread, so the database
// access here is of no concern.
for (int i = 0; i < targetGUIDs.length; i++) {
processor.sendURIToClientForDisplay(url, targetGUIDs[i], title, accountGUID, context);
}
// Request an immediate sync to push these new commands to the network ASAP.
Log.i(LOGTAG, "Requesting immediate clients stage sync.");
tabSender.sync();
return Result.SUCCESS;
// ... Probably.
}
/**
* Get an Intent suitable for broadcasting the UI state of this ShareMethod.
* The caller shall populate the intent with the actual state.
*/
private Intent getUIStateIntent() {
Intent uiStateIntent = new Intent(OverlayConstants.SHARE_METHOD_UI_EVENT);
uiStateIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) Type.SEND_TAB);
return uiStateIntent;
}
/**
* Broadcast the given intent to any UIs that may be listening.
*/
private void broadcastUIState(Intent uiStateIntent) {
LocalBroadcastManager.getInstance(context).sendBroadcast(uiStateIntent);
}
/**
* Load the state of the user's Firefox Sync accounts and broadcast it to any registered
* listeners. This will cause any UIs that may exist that depend on this information to update.
*/
public SendTab(Context aContext) {
super(aContext);
// Initialise the UI state intent...
// Determine if the user has a new or old style sync account and load the available sync
// clients for it.
final AccountManager accountManager = AccountManager.get(context);
final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
if (fxAccounts.length > 0) {
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, fxAccounts[0]);
if (fxAccount.getState().getNeededAction() != State.Action.None) {
// We have a Firefox Account, but it's definitely not able to send a tab
// right now. Redirect to the status activity.
Log.w(LOGTAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() +
" needs action before it can send a tab; redirecting to status activity.");
setOverrideIntent(FxAccountStatusActivity.class);
return;
}
tabSender = new FxAccountTabSender(fxAccount);
updateClientList(tabSender);
Log.i(LOGTAG, "Allowing tab send for Firefox Account.");
registerDisplayURICommand();
return;
}
final Account[] syncAccounts = accountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC);
if (syncAccounts.length > 0) {
tabSender = new Sync11TabSender(context, syncAccounts[0], accountManager);
updateClientList(tabSender);
Log.i(LOGTAG, "Allowing tab send for Sync account.");
registerDisplayURICommand();
return;
}
// Have registered UIs offer to set up a Firefox Account.
setOverrideIntent(FxAccountGetStartedActivity.class);
}
/**
* Load the list of Sync clients that are not this device using the given TabSender.
*/
private void updateClientList(TabSender tabSender) {
Collection<ClientRecord> otherClients = getOtherClients(tabSender);
ParcelableClientRecord[] records = new ParcelableClientRecord[otherClients.size()];
validGUIDs = new HashSet<>();
int i = 0;
// Put the list of ClientRecords into the uiStateIntent and broadcast it.
for (ClientRecord client : otherClients) {
ParcelableClientRecord record = ParcelableClientRecord.fromClientRecord(client);
records[i] = record;
validGUIDs.add(record.guid);
i++;
}
Intent uiStateIntent = getUIStateIntent();
uiStateIntent.putExtra(EXTRA_CLIENT_RECORDS, records);
broadcastUIState(uiStateIntent);
}
/**
* Record our intention to redirect the user to a different activity when they attempt to share
* with us, usually because we found something wrong with their Sync account (a need to login,
* register, etc.)
* This will be recorded in the OVERRIDE_INTENT field of the UI broadcast. Consumers should
* dispatch this intent instead of attempting to share with this ShareMethod whenever it is
* non-null.
*
* @param activityClass The class of the activity we wish to launch instead of invoking a share.
*/
protected void setOverrideIntent(Class<? extends Activity> activityClass) {
Intent intent = new Intent(context, activityClass);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Intent uiStateIntent = getUIStateIntent();
uiStateIntent.putExtra(OVERRIDE_INTENT, intent);
broadcastUIState(uiStateIntent);
}
private static void registerDisplayURICommand() {
final CommandProcessor processor = CommandProcessor.getProcessor();
processor.registerCommand("displayURI", new CommandRunner(3) {
@Override
public void executeCommand(final GlobalSession session, List<String> args) {
CommandProcessor.displayURI(args, session.getContext());
}
});
}
/**
* @return A map from GUID to client record for all sync clients, including our own; or null iff
* ClientsDatabaseAccessor.fetchAllClients throws NullCursorException.
*/
protected Map<String, ClientRecord> getAllClients() {
ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context);
try {
return db.fetchAllClients();
} catch (NullCursorException e) {
Log.w(LOGTAG, "NullCursorException while populating device list.", e);
return null;
} finally {
db.close();
}
}
/**
* @return a collection of client records, excluding our own.
*/
protected Collection<ClientRecord> getOtherClients(final TabSender sender) {
if (sender == null) {
Log.w(LOGTAG, "No tab sender when fetching other client IDs.");
return Collections.emptyList();
}
final Map<String, ClientRecord> all = getAllClients();
if (all == null) {
return Collections.emptyList();
}
final String ourGUID = sender.getAccountGUID();
if (ourGUID == null) {
return all.values();
}
final ArrayList<ClientRecord> out = new ArrayList<>(all.size());
for (Map.Entry<String, ClientRecord> entry : all.entrySet()) {
if (!ourGUID.equals(entry.getKey())) {
out.add(entry.getValue());
}
}
return out;
}
@Override
public String getSuccessMesssage() {
return context.getResources().getString(R.string.sync_text_tab_sent);
}
@Override
public String getFailureMessage() {
return context.getResources().getString(R.string.sync_text_tab_not_sent);
}
/**
* Inteface for interacting with Sync accounts. Used to hide the difference in implementation
* between FXA and "old sync" accounts when sending tabs.
*/
private interface TabSender {
public static final String[] STAGES_TO_SYNC = new String[] { "clients", "tabs" };
/**
* @return Return null if the account isn't correctly initialized. Return
* the account GUID otherwise.
*/
String getAccountGUID();
/**
* Sync this account, specifying only clients and tabs as the engines to sync.
*/
void sync();
}
private static class FxAccountTabSender implements TabSender {
private final AndroidFxAccount fxAccount;
public FxAccountTabSender(AndroidFxAccount fxa) {
fxAccount = fxa;
}
@Override
public String getAccountGUID() {
try {
final SharedPreferences prefs = fxAccount.getSyncPrefs();
return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
} catch (Exception e) {
Log.w(LOGTAG, "Could not get Firefox Account parameters or preferences; aborting.");
return null;
}
}
@Override
public void sync() {
fxAccount.requestSync(FirefoxAccounts.FORCE, STAGES_TO_SYNC, null);
}
}
private static class Sync11TabSender implements TabSender {
private final Account account;
private final AccountManager accountManager;
private final Context context;
private Sync11TabSender(Context aContext, Account syncAccount, AccountManager manager) {
context = aContext;
account = syncAccount;
accountManager = manager;
}
@Override
public String getAccountGUID() {
try {
SharedPreferences prefs = SyncAccounts.blockingPrefsFromDefaultProfileV0(context, accountManager, account);
return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
} catch (Exception e) {
Log.w(LOGTAG, "Could not get Sync account parameters or preferences; aborting.");
return null;
}
}
@Override
public void sync() {
SyncAdapter.requestImmediateSync(account, STAGES_TO_SYNC);
}
}
}

View File

@ -0,0 +1,92 @@
/*This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.service.sharemethods;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Represents a method of sharing a URL/title. Add a bookmark? Send to a device? Add to reading list?
*/
public abstract class ShareMethod {
protected final Context context;
public ShareMethod(Context aContext) {
context = aContext;
}
/**
* Perform a share for the given title/URL combination. Called on the background thread by the
* handler service when a request is made. The "extra" parameter is provided should a ShareMethod
* desire to handle the share differently based on some additional parameters.
*
* @param title The page title for the page being shared. May be null if none can be found.
* @param url The URL of the page to be shared. Never null.
* @param extra A Parcelable of ShareMethod-specific parameters that may be provided by the
* caller. Generally null, but this field may be used to provide extra input to
* the ShareMethod (such as the device to share to in the case of SendTab).
* @return true if the attempt to share was a success. False in the event of an error.
*/
public abstract Result handle(String title, String url, Parcelable extra);
/**
* Convenience method for calling handlers on objects that don't require extra data.
*/
public Result handle(String title, String url) {
return handle(title, url, null);
}
public abstract String getSuccessMesssage();
public abstract String getFailureMessage();
/**
* Enum representing the possible results of performing a share.
*/
public static enum Result {
// Victory!
SUCCESS,
// Failure, but retrying the same action again might lead to success.
TRANSIENT_FAILURE,
// Failure, and you're not going to succeed until you reinitialise the ShareMethod (ie.
// until you repeat the entire share action). Examples include broken Sync accounts, or
// Sync accounts with no valid target devices (so the only way to fix this is to add some
// and try again: pushing a retry button isn't sane).
PERMANENT_FAILURE
}
/**
* Enum representing types of ShareMethod. Parcelable so it may be efficiently used in Intents.
*/
public static enum Type implements Parcelable {
ADD_BOOKMARK,
ADD_TO_READING_LIST,
SEND_TAB;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeInt(ordinal());
}
public static final Creator<Type> CREATOR = new Creator<Type>() {
@Override
public Type createFromParcel(final Parcel source) {
return Type.values()[source.readInt()];
}
@Override
public Type[] newArray(final int size) {
return new Type[size];
}
};
}
}

View File

@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.ui;
import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.mozilla.gecko.R;
/**
* Static helper class for generating toasts for share events.
*
* The overlay toasts come in a variety of flavours: success (rectangle with happy green tick,
* failure (no tick, a retry button), and success-with-tutorial (as success, but with a pretty
* picture of some description to educate the user on how to use the feature) TODO: Bug 1048645.
*/
public class OverlayToastHelper {
/**
* Show a toast indicating a failure to share.
* @param context Context in which to inflate the toast.
* @param failureMessage String to display in the toast.
* @param isTransient Should a retry button be presented?
* @param retryListener Listener to fire when the retry button is pressed.
*/
public static void showFailureToast(Context context, String failureMessage, View.OnClickListener retryListener) {
showToast(context, failureMessage, false, retryListener);
}
public static void showFailureToast(Context context, String failureMessage) {
showFailureToast(context, failureMessage, null);
}
/**
* Show a toast indicating a successful share.
* @param successMessage Message to show in the toast.
*/
public static void showSuccessToast(Context context, String successMessage) {
showToast(context, successMessage, true, null);
}
private static void showToast(Context context, String message, boolean success, View.OnClickListener retryListener) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.overlay_share_toast, null);
TextView text = (TextView) layout.findViewById(R.id.overlay_toast_message);
text.setText(message);
if (retryListener == null) {
// Hide the retry button.
layout.findViewById(R.id.overlay_toast_separator).setVisibility(View.GONE);
layout.findViewById(R.id.overlay_toast_retry_btn).setVisibility(View.GONE);
} else {
// Set up the button to perform a retry.
Button retryBtn = (Button) layout.findViewById(R.id.overlay_toast_retry_btn);
retryBtn.setOnClickListener(retryListener);
}
if (!success) {
// Hide the happy green tick.
text.setCompoundDrawables(null, null, null, null);
}
Toast toast = new Toast(context);
toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.BOTTOM, 0, 0);
toast.setDuration(Toast.LENGTH_SHORT);
toast.setView(layout);
toast.show();
}
}

View File

@ -0,0 +1,178 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.R;
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
import java.util.Collection;
import static org.mozilla.gecko.overlays.ui.SendTabList.*;
public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClientRecord> {
private static final String LOGTAG = "GeckoSendTabAdapter";
private State currentState;
// String to display when in a "button-like" special state. Instead of using a
// ParcelableClientRecord we override the rendering using this string.
private String dummyRecordName;
private final SendTabTargetSelectedListener listener;
private Collection<ParcelableClientRecord> records;
// The AlertDialog to show in the event the record is pressed while in the SHOW_DEVICES state.
// This will show the user a prompt to select a device from a longer list of devices.
private AlertDialog dialog;
public SendTabDeviceListArrayAdapter(Context context, SendTabTargetSelectedListener aListener, int textViewResourceId) {
super(context, textViewResourceId);
listener = aListener;
// We do this manually and avoid multiple notifications when doing compound operations.
setNotifyOnChange(false);
}
/**
* Get an array of the contents of this adapter were it in the LIST state.
* Useful for determining the "real" contents of the adapter.
*/
public ParcelableClientRecord[] toArray() {
return records.toArray(new ParcelableClientRecord[records.size()]);
}
public void setClientRecordList(Collection<ParcelableClientRecord> clientRecordList) {
records = clientRecordList;
updateRecordList();
}
/**
* Ensure the contents of the Adapter are synchronised with the `records` field. This may not
* be the case if records has recently changed, or if we have experienced a state change.
*/
public void updateRecordList() {
if (currentState != State.LIST) {
return;
}
clear();
setNotifyOnChange(false); // So we don't notify for each add.
if (AppConstants.Versions.feature11Plus) {
addAll(records);
} else {
for (ParcelableClientRecord record : records) {
add(record);
}
}
notifyDataSetChanged();
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
final Context context = getContext();
// Reuse View objects if they exist.
TextView row = (TextView) convertView;
if (row == null) {
row = (TextView) View.inflate(context, R.layout.overlay_share_send_tab_item, null);
}
if (currentState != State.LIST) {
// If we're in a special "Button-like" state, use the override string and a generic icon.
row.setText(dummyRecordName);
row.setCompoundDrawablesWithIntrinsicBounds(R.drawable.overlay_send_tab_icon, 0, 0, 0);
}
// If we're just a button to launch the dialog, set the listener and abort.
if (currentState == State.SHOW_DEVICES) {
row.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
dialog.show();
}
});
return row;
}
// The remaining states delegate to the SentTabTargetSelectedListener.
final String listenerGUID;
ParcelableClientRecord clientRecord = getItem(position);
if (currentState == State.LIST) {
row.setText(clientRecord.name);
row.setCompoundDrawablesWithIntrinsicBounds(getImage(clientRecord), 0, 0, 0);
listenerGUID = clientRecord.guid;
} else {
listenerGUID = null;
}
row.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
listener.onSendTabTargetSelected(listenerGUID);
}
});
return row;
}
private static int getImage(ParcelableClientRecord record) {
if ("mobile".equals(record.type)) {
return R.drawable.sync_mobile;
}
return R.drawable.sync_desktop;
}
public void switchState(State newState) {
if (currentState == newState) {
return;
}
currentState = newState;
switch (newState) {
case LIST:
updateRecordList();
break;
case NONE:
showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_tab_btn_label));
break;
case SHOW_DEVICES:
showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_other));
break;
default:
Assert.isTrue(false, "Unexpected state transition: " + newState);
}
}
/**
* Set the dummy override string to the given value and clear the list.
*/
private void showDummyRecord(String name) {
dummyRecordName = name;
clear();
add(null);
notifyDataSetChanged();
}
public void setDialog(AlertDialog aDialog) {
dialog = aDialog;
}
}

View File

@ -0,0 +1,170 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ListAdapter;
import android.widget.ListView;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.R;
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
import java.util.Arrays;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.LIST;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.LOADING;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.NONE;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.SHOW_DEVICES;
/**
* The SendTab button has a few different states depending on the available devices (and whether
* we've loaded them yet...)
*
* Initially, the view resembles a disabled button. (the LOADING state)
* Once state is loaded from Sync's database, we know how many devices the user may send their tab
* to.
*
* If there are no targets, the user was found to not have a Sync account, or their Sync account is
* in a state that prevents it from being able to send a tab, we enter the NONE state and display
* a generic button which launches an appropriate activity to fix the situation when tapped (such
* as the set up Sync wizard).
*
* If the number of targets does not MAX_INLINE_SYNC_TARGETS, we present a button for each of them.
* (the LIST state)
*
* Otherwise, we enter the SHOW_DEVICES state, in which we display a "Send to other devices" button
* that takes the user to a menu for selecting a target device from their complete list of many
* devices.
*/
public class SendTabList extends ListView {
private static final String LOGTAG = "SendTabList";
// The maximum number of target devices to show in the main list. Further devices are available
// from a secondary menu.
public static final int MAXIMUM_INLINE_ELEMENTS = 2;
private SendTabDeviceListArrayAdapter clientListAdapter;
// Listener to fire when a share target is selected (either directly or via the prompt)
private SendTabTargetSelectedListener listener;
private State currentState = LOADING;
/**
* Enum defining the states this view may occupy.
*/
public enum State {
// State when no sync targets exist (a generic "Send to Firefox Sync" button which launches
// an activity to set it up)
NONE,
// As NONE, but disabled. Initial state. Used until we get information from Sync about what
// we really want.
LOADING,
// A list of devices to share to.
LIST,
// A single button prompting the user to select a device to share to.
SHOW_DEVICES
}
public SendTabList(Context context) {
super(context);
}
public SendTabList(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void setAdapter(ListAdapter adapter) {
Assert.isTrue(adapter instanceof SendTabDeviceListArrayAdapter);
clientListAdapter = (SendTabDeviceListArrayAdapter) adapter;
super.setAdapter(adapter);
}
public void setSendTabTargetSelectedListener(SendTabTargetSelectedListener aListener) {
listener = aListener;
}
public void switchState(State state) {
if (state == currentState) {
return;
}
clientListAdapter.switchState(state);
if (state == SHOW_DEVICES) {
clientListAdapter.setDialog(getDialog());
}
}
public void setSyncClients(ParcelableClientRecord[] clients) {
if (clients == null) {
clients = new ParcelableClientRecord[0];
}
int size = clients.length;
if (size == 0) {
// Just show a button to set up sync (or whatever).
switchState(NONE);
return;
}
clientListAdapter.setClientRecordList(Arrays.asList(clients));
if (size <= MAXIMUM_INLINE_ELEMENTS) {
// Show the list of devices inline.
switchState(LIST);
return;
}
// Just show a button to launch the list of devices to choose one from.
switchState(SHOW_DEVICES);
}
/**
* Get an AlertDialog listing all devices, allowing the user to select the one they want.
* Used when more than MAXIMUM_INLINE_ELEMENTS devices are found (to avoid displaying them all
* inline and looking crazy.
*/
public AlertDialog getDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
final ParcelableClientRecord[] records = clientListAdapter.toArray();
final String[] dialogElements = new String[records.length];
for (int i = 0; i < records.length; i++) {
dialogElements[i] = records[i].name;
}
builder.setTitle(R.string.overlay_share_select_device)
.setItems(dialogElements, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int index) {
listener.onSendTabTargetSelected(records[index].guid);
}
});
return builder.create();
}
/**
* Prevent scrolling of this ListView.
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
return true;
}
return super.dispatchTouchEvent(ev);
}
}

View File

@ -0,0 +1,18 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mozilla.gecko.overlays.ui;
/**
* Interface for classes that wish to listen for the selection of an element from a SendTabList.
*/
public interface SendTabTargetSelectedListener {
/**
* Called when a row in the SendTabList is clicked.
*
* @param targetGUID The GUID of the ClientRecord the element represents (if any, otherwise null)
*/
public void onSendTabTargetSelected(String targetGUID);
}

View File

@ -0,0 +1,284 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.TextView;
import android.widget.Toast;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.R;
import org.mozilla.gecko.overlays.OverlayConstants;
import org.mozilla.gecko.overlays.service.OverlayActionService;
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
import org.mozilla.gecko.LocaleAware;
import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
/**
* A transparent activity that displays the share overlay.
*/
public class ShareDialog extends LocaleAware.LocaleAwareActivity implements SendTabTargetSelectedListener {
private static final String LOGTAG = "GeckoShareDialog";
private String url;
private String title;
// The override intent specified by SendTab (if any). See SendTab.java.
private Intent sendTabOverrideIntent;
// Flag set during animation to prevent animation multiple-start.
private boolean isAnimating;
// BroadcastReceiver to receive callbacks from ShareMethods which are changing state.
private final BroadcastReceiver uiEventListener = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
ShareMethod.Type originShareMethod = intent.getParcelableExtra(OverlayConstants.EXTRA_SHARE_METHOD);
switch (originShareMethod) {
case SEND_TAB:
handleSendTabUIEvent(intent);
break;
default:
throw new IllegalArgumentException("UIEvent broadcast from ShareMethod that isn't thought to support such broadcasts.");
}
}
};
/**
* Called when a UI event broadcast is received from the SendTab ShareMethod.
*/
protected void handleSendTabUIEvent(Intent intent) {
sendTabOverrideIntent = intent.getParcelableExtra(SendTab.OVERRIDE_INTENT);
SendTabList sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
ParcelableClientRecord[] clientrecords = (ParcelableClientRecord[]) intent.getParcelableArrayExtra(SendTab.EXTRA_CLIENT_RECORDS);
sendTabList.setSyncClients(clientrecords);
}
@Override
protected void onDestroy() {
// Remove the listener when the activity is destroyed: we no longer care.
// Note: The activity can be destroyed without onDestroy being called. However, this occurs
// only when the application is killed, something which also kills the registered receiver
// list, and the service, and everything else: so we don't care.
LocalBroadcastManager.getInstance(this).unregisterReceiver(uiEventListener);
super.onDestroy();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setWindowAnimations(0);
Intent intent = getIntent();
// The URL is usually hiding somewhere in the extra text. Extract it.
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
String pageUrl = new WebURLFinder(extraText).bestWebURL();
if (TextUtils.isEmpty(pageUrl)) {
Log.e(LOGTAG, "Unable to process shared intent. No URL found!");
// Display toast notifying the user of failure (most likely a developer who screwed up
// trying to send a share intent).
Toast toast = Toast.makeText(this, getResources().getText(R.string.overlay_share_no_url), Toast.LENGTH_SHORT);
toast.show();
return;
}
setContentView(R.layout.overlay_share_dialog);
LocalBroadcastManager.getInstance(this).registerReceiver(uiEventListener,
new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT));
// Have the service start any initialisation work that's necessary for us to show the correct
// UI. The results of such work will come in via the BroadcastListener.
Intent serviceStartupIntent = new Intent(this, OverlayActionService.class);
serviceStartupIntent.setAction(OverlayConstants.ACTION_PREPARE_SHARE);
startService(serviceStartupIntent);
// If provided, we use the subject text to give us something nice to display.
// If not, we wing it with the URL.
// TODO: Consider polling Fennec databases to find better information to display.
String subjectText = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subjectText != null) {
((TextView) findViewById(R.id.title)).setText(subjectText);
}
title = subjectText;
url = pageUrl;
// Set the subtitle text on the view and cause it to marquee if it's too long (which it will
// be, since it's a URL).
TextView subtitleView = (TextView) findViewById(R.id.subtitle);
subtitleView.setText(pageUrl);
subtitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
subtitleView.setSingleLine(true);
subtitleView.setMarqueeRepeatLimit(5);
subtitleView.setSelected(true);
// Start the slide-up animation.
Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_up);
findViewById(R.id.sharedialog).startAnimation(anim);
// Add button event listeners.
findViewById(R.id.overlay_share_bookmark_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addBookmark();
}
});
findViewById(R.id.overlay_share_reading_list_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addToReadingList();
}
});
// Send tab.
SendTabList sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
// Register ourselves as both the listener and the context for the Adapter.
SendTabDeviceListArrayAdapter adapter = new SendTabDeviceListArrayAdapter(this, this, R.layout.sync_list_item);
sendTabList.setAdapter(adapter);
sendTabList.setSendTabTargetSelectedListener(this);
}
/**
* Helper method to get an overlay service intent populated with the data held in this dialog.
*/
private Intent getServiceIntent(ShareMethod.Type method) {
final Intent serviceIntent = new Intent(this, OverlayActionService.class);
serviceIntent.setAction(OverlayConstants.ACTION_SHARE);
serviceIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) method);
serviceIntent.putExtra(OverlayConstants.EXTRA_URL, url);
serviceIntent.putExtra(OverlayConstants.EXTRA_TITLE, title);
return serviceIntent;
}
@Override
public void finish() {
super.finish();
// Don't perform an activity-dismiss animation.
overridePendingTransition(0, 0);
}
/*
* Button handlers. Send intents to the background service responsible for processing requests
* on Fennec in the background. (a nice extensible mechanism for "doing stuff without properly
* launching Fennec").
*/
public void sendTab(String targetGUID) {
// If an override intent has been set, dispatch it.
if (sendTabOverrideIntent != null) {
startActivity(sendTabOverrideIntent);
finish();
return;
}
// targetGUID being null with no override intent should be an impossible state.
Assert.isTrue(targetGUID != null);
Intent serviceIntent = getServiceIntent(ShareMethod.Type.SEND_TAB);
// Currently, only one extra parameter is necessary (the GUID of the target device).
Bundle extraParameters = new Bundle();
// Future: Handle multiple-selection. Bug 1061297.
extraParameters.putStringArray(SendTab.SEND_TAB_TARGET_DEVICES, new String[] { targetGUID });
serviceIntent.putExtra(OverlayConstants.EXTRA_PARAMETERS, extraParameters);
startService(serviceIntent);
slideOut();
}
@Override
public void onSendTabTargetSelected(String targetGUID) {
sendTab(targetGUID);
}
public void addToReadingList() {
startService(getServiceIntent(ShareMethod.Type.ADD_TO_READING_LIST));
slideOut();
}
public void addBookmark() {
startService(getServiceIntent(ShareMethod.Type.ADD_BOOKMARK));
slideOut();
}
/**
* Slide the overlay down off the screen and destroy it.
*/
private void slideOut() {
if (isAnimating) {
return;
}
isAnimating = true;
Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_down);
findViewById(R.id.sharedialog).startAnimation(anim);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
// Unused. I can haz Miranda method?
}
@Override
public void onAnimationEnd(Animation animation) {
finish();
}
@Override
public void onAnimationRepeat(Animation animation) {
// Unused.
}
});
}
/**
* Close the dialog if back is pressed.
*/
@Override
public void onBackPressed() {
slideOut();
}
/**
* Close the dialog if the anything that isn't a button is tapped.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
slideOut();
return true;
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_longAnimTime"
android:fromYDelta="0"
android:toYDelta="100%p" />

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_longAnimTime"
android:fromYDelta="100%p"
android:toYDelta="0" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!-- Serves to position the content on the screen (bottom, centered) and provide the drop-shadow -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sharedialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="-12dp"
android:paddingTop="30dp"
android:layout_gravity="bottom|center"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout
android:id="@+id/share_overlay_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:orientation="vertical"
android:background="@drawable/share_overlay_background">
<!-- Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:background="@color/background_light"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="15dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:layout_gravity="center">
<!-- Title -->
<TextView
android:id="@id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="7dp"
android:ellipsize="end"
android:maxLines="2"
android:scrollHorizontally="true"
android:textColor="@color/text_color_primary"
android:textSize="17sp"/>
<!-- Subtitle (url) -->
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_color_secondary"/>
</LinearLayout>
<!-- Buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/overlay_share_background_colour"
android:orientation="vertical">
<!-- TODO: Once API 11 is available, stick "showDividers=middle" into the parent and get rid
of these evil separator views. -->
<!-- "Send to Firefox Sync" -->
<org.mozilla.gecko.overlays.ui.SendTabList
style="@style/ShareOverlayButton"
android:id="@+id/overlay_send_tab_btn"
android:background="@color/overlay_share_background_colour"
android:padding="0dp"/>
<!-- Evil separator -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/background_light"/>
<!-- "Add to reading list" -->
<TextView
style="@style/ShareOverlayButton.Text"
android:id="@+id/overlay_share_reading_list_btn"
android:text="@string/overlay_share_reading_list_btn_label"
android:drawableLeft="@drawable/overlay_readinglist_icon"/>
<!-- Evil separator -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/background_light"/>
<!-- "Add bookmark" -->
<TextView
style="@style/ShareOverlayButton.Text"
android:id="@+id/overlay_share_bookmark_btn"
android:text="@string/overlay_share_bookmark_btn_label"
android:drawableLeft="@drawable/overlay_bookmark_icon"/>
</LinearLayout>
</LinearLayout>
<!-- Firefox logo (has to appear higher in the z-order than the content. -->
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/share_overlay_content"
android:scaleType="center"
android:layout_centerHorizontal="true"
android:src="@drawable/icon"
android:layout_marginBottom="-6dp"/>
</RelativeLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ListView
style="@style/ShareOverlayButton"
android:id="@+id/device_list"
android:padding="0dp" >
</ListView>
</LinearLayout>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/ShareOverlayButton.Text"/>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/overlay_share_toast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/share_overlay_background"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_gravity="bottom|center">
<!-- Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:background="@color/background_light"
android:orientation="horizontal"
android:paddingLeft="5dp"
android:paddingRight="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp">
<!-- Large attractive green tick with label to the right -->
<TextView
style="@style/ShareOverlayButton.Text"
android:id="@+id/overlay_toast_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:textColor="@color/text_color_primary"
android:textSize="14sp"
android:drawableLeft="@drawable/overlay_check"/>
<!-- Evil separator -->
<View
android:id="@+id/overlay_toast_separator"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:layout_height="match_parent"
android:layout_width="1dp"
android:background="@color/background_light"/>
<!-- Retry button -->
<Button
android:id="@+id/overlay_toast_retry_btn"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/overlay_share_retry"
android:onClick="selfDestruct" />
</LinearLayout>
</LinearLayout>

View File

@ -48,6 +48,12 @@
<color name="text_color_secondary_inverse">#DDDDDD</color>
<color name="text_color_tertiary_inverse">#A4A7A9</color>
<!-- Colour used for share overlay button labels -->
<color name="text_color_overlaybtn">#666666</color>
<!-- Colour used for share overlay button background -->
<color name="overlay_share_background_colour">#FFD0CECB</color>
<!-- Disabled colors -->
<color name="text_color_primary_disable_only">#999999</color>

View File

@ -763,6 +763,29 @@
<item name="android:gravity">right</item>
</style>
<style name="ShareOverlayButton">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:minHeight">60dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:paddingLeft">15dp</item>
<item name="android:paddingRight">15dp</item>
<item name="android:paddingTop">17dp</item>
<item name="android:paddingBottom">17dp</item>
<item name="android:focusableInTouchMode">false</item>
<item name="android:clickable">true</item>
<item name="android:background">@android:drawable/list_selector_background</item>
<item name="android:layout_margin">0dp</item>
</style>
<style name="ShareOverlayButton.Text">
<item name="android:drawablePadding">15dp</item>
<item name="android:maxLines">1</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">@color/text_color_overlaybtn</item>
<item name="android:ellipsize">marquee</item>
</style>
<style name="TabInput"></style>
<style name="TabInput.TabWidget">
@ -780,6 +803,13 @@
<item name="android:paddingTop">0dp</item>
</style>
<!-- Make the share overlay activity appear like an overlay. -->
<style name="ShareOverlayActivity">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:backgroundDimEnabled">true</item>
</style>
<style name="OnboardStartLayout">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>

View File

@ -110,6 +110,15 @@
<string name="media_pause">&media_pause;</string>
<string name="media_stop">&media_stop;</string>
<string name="overlay_share_send_other">&overlay_share_send_other;</string>
<string name="overlay_share_header">&overlay_share_header;</string>
<string name="overlay_share_bookmark_btn_label">&overlay_share_bookmark_btn_label;</string>
<string name="overlay_share_reading_list_btn_label">&overlay_share_reading_list_btn_label;</string>
<string name="overlay_share_send_tab_btn_label">&overlay_share_send_tab_btn_label;</string>
<string name="overlay_share_no_url">&overlay_share_no_url;</string>
<string name="overlay_share_retry">&overlay_share_retry;</string>
<string name="overlay_share_select_device">&overlay_share_select_device;</string>
<string name="settings">&settings;</string>
<string name="settings_title">&settings_title;</string>
<string name="pref_category_advanced">&pref_category_advanced;</string>

View File

@ -49,10 +49,10 @@ public class BackButton extends ShapedButton {
super.onSizeChanged(width, height, oldWidth, oldHeight);
mPath.reset();
mPath.addCircle(width/2, height/2, width/2 - mBorderWidth, Path.Direction.CW);
mPath.addCircle(width/2, height/2, width/2, Path.Direction.CW);
mBorderPath.reset();
mBorderPath.addCircle(width/2, height/2, (width/2) - mBorderWidth, Path.Direction.CW);
mBorderPath.addCircle(width/2, height/2, (width/2) - (mBorderWidth/2), Path.Direction.CW);
}
@Override

View File

@ -81,5 +81,8 @@ else
MOZ_ANDROID_SEARCH_ACTIVITY=
fi
# Don't enable the share overlay.
# MOZ_ANDROID_SHARE_OVERLAY=1
# Don't enable the Mozilla Location Service stumbler.
# MOZ_ANDROID_MLS_STUMBLER=1

View File

@ -74,6 +74,7 @@
</intent-filter>
</receiver>
#ifndef MOZ_ANDROID_SHARE_OVERLAY
<activity
android:theme="@style/SyncTheme"
android:excludeFromRecents="true"
@ -90,3 +91,4 @@
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
#endif

View File

@ -4032,8 +4032,9 @@ DebuggerServer.ObjectActorPreviewers.Object = [
preview.modifiers = modifiers;
props.push("key", "charCode", "keyCode");
} else if (aRawObj instanceof Ci.nsIDOMTransitionEvent ||
aRawObj instanceof Ci.nsIDOMAnimationEvent) {
} else if (aRawObj instanceof Ci.nsIDOMTransitionEvent) {
props.push("propertyName", "pseudoElement");
} else if (aRawObj instanceof Ci.nsIDOMAnimationEvent) {
props.push("animationName", "pseudoElement");
} else if (aRawObj instanceof Ci.nsIDOMClipboardEvent) {
props.push("clipboardData");