Merge m-c to m-i, a=merge
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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 -->
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -4,6 +4,6 @@
|
||||
"remote": "",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "bce92f813f48346b36ce9dffc16b0c93d0ac8330",
|
||||
"revision": "14400ee07e836d74039e2317feebc0a13fcb60c8",
|
||||
"repo_path": "/integration/gaia-central"
|
||||
}
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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 -->
|
||||
|
@ -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"/>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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}),
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -55,8 +55,6 @@ body,
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.footer,
|
||||
.footer a,
|
||||
.terms-service,
|
||||
.terms-service a {
|
||||
font-size: .6rem;
|
||||
|
@ -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")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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}}
|
||||
|
@ -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});
|
||||
|
@ -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]
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
});
|
@ -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"})
|
||||
)
|
||||
),
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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__
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
68
mobile/android/base/overlays/OverlayConstants.java
Normal 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";
|
||||
}
|
148
mobile/android/base/overlays/service/OverlayActionService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
370
mobile/android/base/overlays/service/sharemethods/SendTab.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
76
mobile/android/base/overlays/ui/OverlayToastHelper.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
170
mobile/android/base/overlays/ui/SendTabList.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
284
mobile/android/base/overlays/ui/ShareDialog.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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" />
|
9
mobile/android/base/resources/anim/overlay_slide_up.xml
Normal 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" />
|
After Width: | Height: | Size: 652 B |
BIN
mobile/android/base/resources/drawable-hdpi/overlay_check.png
Normal file
After Width: | Height: | Size: 391 B |
After Width: | Height: | Size: 340 B |
After Width: | Height: | Size: 325 B |
After Width: | Height: | Size: 477 B |
BIN
mobile/android/base/resources/drawable-mdpi/overlay_check.png
Normal file
After Width: | Height: | Size: 308 B |
After Width: | Height: | Size: 336 B |
After Width: | Height: | Size: 307 B |
After Width: | Height: | Size: 755 B |
BIN
mobile/android/base/resources/drawable-xhdpi/overlay_check.png
Normal file
After Width: | Height: | Size: 422 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 951 B |
BIN
mobile/android/base/resources/drawable-xxhdpi/overlay_check.png
Normal file
After Width: | Height: | Size: 509 B |
After Width: | Height: | Size: 451 B |
After Width: | Height: | Size: 460 B |
117
mobile/android/base/resources/layout/overlay_share_dialog.xml
Normal 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>
|
@ -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>
|
@ -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"/>
|
58
mobile/android/base/resources/layout/overlay_share_toast.xml
Normal 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>
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|