mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-06 15:27:30 +09:00
Issue #1053 - Drop support Android and remove Fennec - Part 1a: Remove mobile/android
This commit is contained in:
parent
85045bf6be
commit
c9b411d6cd
3891 changed files with 0 additions and 428084 deletions
|
|
@ -127,7 +127,6 @@ MACH_MODULES = [
|
||||||
'tools/lint/mach_commands.py',
|
'tools/lint/mach_commands.py',
|
||||||
'tools/mach_commands.py',
|
'tools/mach_commands.py',
|
||||||
'tools/power/mach_commands.py',
|
'tools/power/mach_commands.py',
|
||||||
'mobile/android/mach_commands.py',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if os.path.exists('addon-sdk/mach_commands.py'):
|
if os.path.exists('addon-sdk/mach_commands.py'):
|
||||||
|
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
env:
|
|
||||||
browser: true
|
|
||||||
|
|
||||||
globals:
|
|
||||||
Components: false
|
|
||||||
|
|
||||||
# TODO: Create custom rule for `Cu.import`
|
|
||||||
AddonManager: false
|
|
||||||
AppConstants: false
|
|
||||||
Downloads: false
|
|
||||||
File: false
|
|
||||||
FileUtils: false
|
|
||||||
HelperApps: true # TODO: Can be more specific here.
|
|
||||||
JNI: true # TODO: Can be more specific here.
|
|
||||||
LightweightThemeManager: false
|
|
||||||
Messaging: false
|
|
||||||
Notifications: false
|
|
||||||
OS: false
|
|
||||||
ParentalControls: false
|
|
||||||
PrivateBrowsingUtils: false
|
|
||||||
Prompt: false
|
|
||||||
Services: false
|
|
||||||
SharedPreferences: false
|
|
||||||
strings: false
|
|
||||||
Strings: false
|
|
||||||
Task: false
|
|
||||||
UITelemetry: false
|
|
||||||
UserAgentOverrides: 0
|
|
||||||
XPCOMUtils: false
|
|
||||||
ctypes: false
|
|
||||||
dump: false
|
|
||||||
exports: false
|
|
||||||
importScripts: false
|
|
||||||
module: false
|
|
||||||
require: false
|
|
||||||
uuidgen: false
|
|
||||||
|
|
||||||
Iterator: false # TODO: Remove - deprecated!
|
|
||||||
|
|
||||||
rules:
|
|
||||||
global-strict: 0 # Overridden by "strict"
|
|
||||||
no-underscore-dangle: 0 # We allow trailing underscores in names.
|
|
||||||
|
|
||||||
# We disable everything to get all files to pass w/o updating them.
|
|
||||||
# We'll re-enable one by one.
|
|
||||||
camelcase: 0
|
|
||||||
comma-dangle: 0
|
|
||||||
comma-spacing: 0
|
|
||||||
consistent-return: 0
|
|
||||||
curly: 0
|
|
||||||
dot-notation: 0
|
|
||||||
eqeqeq: 0
|
|
||||||
key-spacing: 0
|
|
||||||
new-cap: 0
|
|
||||||
no-caller: 0
|
|
||||||
no-constant-condition: 0
|
|
||||||
no-empty: 0
|
|
||||||
no-extra-bind: 0
|
|
||||||
no-extra-semi: 0
|
|
||||||
no-loop-func: 0
|
|
||||||
no-multi-spaces: 0
|
|
||||||
no-new-object: 0
|
|
||||||
no-octal: 0
|
|
||||||
no-return-assign: 0
|
|
||||||
no-shadow: 0
|
|
||||||
no-trailing-spaces: 0
|
|
||||||
no-unused-vars: 0
|
|
||||||
no-use-before-define: 0
|
|
||||||
quotes: 0 # [2, "double"]
|
|
||||||
semi: 0
|
|
||||||
space-infix-ops: 0
|
|
||||||
space-unary-ops: 0 # 2: https://github.com/eslint/eslint/issues/2764
|
|
||||||
strict: 0
|
|
||||||
|
|
||||||
#"ecmaFeatures": {
|
|
||||||
# "forOf": true,
|
|
||||||
# "jsx": true,
|
|
||||||
#},
|
|
||||||
#"rules": {
|
|
||||||
# // turn off all kinds of stuff that we actually do want, because
|
|
||||||
# // right now, we're bootstrapping the linting infrastructure. We'll
|
|
||||||
# // want to audit these rules, and start turning them on and fixing the
|
|
||||||
# // problems they find, one at a time.
|
|
||||||
|
|
||||||
# // Eslint built-in rules are documented at <http://eslint.org/docs/rules/>
|
|
||||||
# "camelcase": 0, // TODO: Remove (use default)
|
|
||||||
# "consistent-return": 0, // TODO: Remove (use default)
|
|
||||||
# dot-location: 0, // [2, property],
|
|
||||||
# "eqeqeq": 0, // TBD. Might need to be separate for content & chrome
|
|
||||||
# "global-strict": 0, // Leave as zero (this will be unsupported in eslint 1.0.0)
|
|
||||||
# "linebreak-style": [2, "unix"],
|
|
||||||
# "new-cap": 0, // TODO: Remove (use default)
|
|
||||||
# "no-catch-shadow": 0, // TODO: Remove (use default)
|
|
||||||
# "no-console": 0, // Leave as 0. We use console logging in content code.
|
|
||||||
# "no-empty": 0, // TODO: Remove (use default)
|
|
||||||
# "no-extra-bind": 0, // Leave as 0
|
|
||||||
# "no-extra-boolean-cast": 0, // TODO: Remove (use default)
|
|
||||||
# "no-multi-spaces": 0, // TBD.
|
|
||||||
# "no-new": 0, // TODO: Remove (use default)
|
|
||||||
# "no-redeclare": 0, // TODO: Remove (use default)
|
|
||||||
# "no-return-assign": 0, // TODO: Remove (use default)
|
|
||||||
# "no-underscore-dangle": 0, // Leave as 0. Commonly used for private variables.
|
|
||||||
# "no-unneeded-ternary": 2,
|
|
||||||
# "no-unused-expressions": 0, // TODO: Remove (use default)
|
|
||||||
# "no-unused-vars": 0, // TODO: Remove (use default)
|
|
||||||
# "no-use-before-define": 0, // TODO: Remove (use default)
|
|
||||||
# "quotes": [2, "double", "avoid-escape"],
|
|
||||||
# "strict": 0, // [2, "function"],
|
|
||||||
#}
|
|
||||||
|
|
@ -1,373 +0,0 @@
|
||||||
Mozilla Public License Version 2.0
|
|
||||||
==================================
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
means each individual or legal entity that creates, contributes to
|
|
||||||
the creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
means the combination of the Contributions of others (if any) used
|
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
means Source Code Form to which the initial Contributor has attached
|
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
|
||||||
including portions thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
(a) that the initial Contributor has attached the notice described
|
|
||||||
in Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
(b) that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the
|
|
||||||
terms of a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
means a work that combines Covered Software with other material, in
|
|
||||||
a separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
means having the right to grant, to the maximum extent possible,
|
|
||||||
whether at the time of the initial grant or subsequently, any and
|
|
||||||
all of the rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
(a) any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered
|
|
||||||
Software; or
|
|
||||||
|
|
||||||
(b) any new file in Source Code Form that contains any Covered
|
|
||||||
Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the
|
|
||||||
License, by the making, using, selling, offering for sale, having
|
|
||||||
made, import, or transfer of either its Contributions or its
|
|
||||||
Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU
|
|
||||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
|
||||||
Public License, Version 3.0, or any later versions of those
|
|
||||||
licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that
|
|
||||||
controls, is controlled by, or is under common control with You. For
|
|
||||||
purposes of this definition, "control" means (a) the power, direct
|
|
||||||
or indirect, to cause the direction or management of such entity,
|
|
||||||
whether by contract or otherwise, or (b) ownership of more than
|
|
||||||
fifty percent (50%) of the outstanding shares or beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
(a) under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
|
||||||
for sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
(a) for any code that a Contributor has removed from Covered Software;
|
|
||||||
or
|
|
||||||
|
|
||||||
(b) for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights
|
|
||||||
to grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
|
||||||
in Section 2.1.
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
(a) such Covered Software must also be made available in Source Code
|
|
||||||
Form, as described in Section 3.1, and You must inform recipients of
|
|
||||||
the Executable Form how they can obtain a copy of such Source Code
|
|
||||||
Form by reasonable means in a timely manner, at a charge no more
|
|
||||||
than the cost of distribution to the recipient; and
|
|
||||||
|
|
||||||
(b) You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter
|
|
||||||
the recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty,
|
|
||||||
or limitations of liability) contained within the Source Code Form of
|
|
||||||
the Covered Software, except that You may alter any license notices to
|
|
||||||
the extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this
|
|
||||||
License with respect to some or all of the Covered Software due to
|
|
||||||
statute, judicial order, or regulation then You must: (a) comply with
|
|
||||||
the terms of this License to the maximum extent possible; and (b)
|
|
||||||
describe the limitations and the code they affect. Such description must
|
|
||||||
be placed in a text file included with all distributions of the Covered
|
|
||||||
Software under this License. Except to the extent prohibited by statute
|
|
||||||
or regulation, such description must be sufficiently detailed for a
|
|
||||||
recipient of ordinary skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
--------------
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically
|
|
||||||
if You fail to comply with any of its terms. However, if You become
|
|
||||||
compliant, then the rights granted under this License from a particular
|
|
||||||
Contributor are reinstated (a) provisionally, unless and until such
|
|
||||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
|
||||||
ongoing basis, if such Contributor fails to notify You of the
|
|
||||||
non-compliance by some reasonable means prior to 60 days after You have
|
|
||||||
come back into compliance. Moreover, Your grants from a particular
|
|
||||||
Contributor are reinstated on an ongoing basis if such Contributor
|
|
||||||
notifies You of the non-compliance by some reasonable means, this is the
|
|
||||||
first time You have received notice of non-compliance with this License
|
|
||||||
from such Contributor, and You become compliant prior to 30 days after
|
|
||||||
Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
|
||||||
end user license agreements (excluding distributors and resellers) which
|
|
||||||
have been validly granted by You or Your distributors under this License
|
|
||||||
prior to termination shall survive termination.
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 6. Disclaimer of Warranty *
|
|
||||||
* ------------------------- *
|
|
||||||
* *
|
|
||||||
* Covered Software is provided under this License on an "as is" *
|
|
||||||
* basis, without warranty of any kind, either expressed, implied, or *
|
|
||||||
* statutory, including, without limitation, warranties that the *
|
|
||||||
* Covered Software is free of defects, merchantable, fit for a *
|
|
||||||
* particular purpose or non-infringing. The entire risk as to the *
|
|
||||||
* quality and performance of the Covered Software is with You. *
|
|
||||||
* Should any Covered Software prove defective in any respect, You *
|
|
||||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
|
||||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
|
||||||
* essential part of this License. No use of any Covered Software is *
|
|
||||||
* authorized under this License except under this disclaimer. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 7. Limitation of Liability *
|
|
||||||
* -------------------------- *
|
|
||||||
* *
|
|
||||||
* Under no circumstances and under no legal theory, whether tort *
|
|
||||||
* (including negligence), contract, or otherwise, shall any *
|
|
||||||
* Contributor, or anyone who distributes Covered Software as *
|
|
||||||
* permitted above, be liable to You for any direct, indirect, *
|
|
||||||
* special, incidental, or consequential damages of any character *
|
|
||||||
* including, without limitation, damages for lost profits, loss of *
|
|
||||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
|
||||||
* and all other commercial damages or losses, even if such party *
|
|
||||||
* shall have been informed of the possibility of such damages. This *
|
|
||||||
* limitation of liability shall not apply to liability for death or *
|
|
||||||
* personal injury resulting from such party's negligence to the *
|
|
||||||
* extent applicable law prohibits such limitation. Some *
|
|
||||||
* jurisdictions do not allow the exclusion or limitation of *
|
|
||||||
* incidental or consequential damages, so this exclusion and *
|
|
||||||
* limitation may not apply to You. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the
|
|
||||||
courts of a jurisdiction where the defendant maintains its principal
|
|
||||||
place of business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions.
|
|
||||||
Nothing in this Section shall prevent a party's ability to bring
|
|
||||||
cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides
|
|
||||||
that the language of a contract shall be construed against the drafter
|
|
||||||
shall not be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses
|
|
||||||
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
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/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
|
||||||
file in a relevant directory) where a recipient would be likely to look
|
|
||||||
for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
defined by the Mozilla Public License, v. 2.0.
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# 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/.
|
|
||||||
|
|
||||||
include $(topsrcdir)/config/rules.mk
|
|
||||||
include $(topsrcdir)/testing/testsuite-targets.mk
|
|
||||||
|
|
||||||
package-mobile-tests:
|
|
||||||
$(MAKE) stage-mochitest DIST_BIN=$(DEPTH)/$(DIST)/bin/xulrunner
|
|
||||||
$(NSINSTALL) -D $(DIST)/$(PKG_PATH)
|
|
||||||
@(cd $(PKG_STAGE) && tar $(TAR_CREATE_FLAGS) - *) | bzip2 -f > $(DIST)/$(PKG_PATH)$(TEST_PACKAGE)
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# vim: set filetype=python:
|
|
||||||
# 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/.
|
|
||||||
|
|
||||||
include('/toolkit/toolkit.mozbuild')
|
|
||||||
|
|
||||||
if CONFIG['ENABLE_TESTS']:
|
|
||||||
DIRS += ['/testing/instrumentation']
|
|
||||||
|
|
||||||
if CONFIG['MOZ_EXTENSIONS']:
|
|
||||||
DIRS += ['/extensions']
|
|
||||||
|
|
||||||
DIRS += [
|
|
||||||
'/%s' % CONFIG['MOZ_BRANDING_DIRECTORY'],
|
|
||||||
'/mobile/android',
|
|
||||||
]
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
This is an example asset.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
File diff suppressed because it is too large
Load diff
|
|
@ -1,404 +0,0 @@
|
||||||
buildDir "${topobjdir}/gradle/build/mobile/android/app"
|
|
||||||
|
|
||||||
apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'checkstyle'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 23
|
|
||||||
buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
targetSdkVersion 23
|
|
||||||
minSdkVersion 15
|
|
||||||
applicationId mozconfig.substs.ANDROID_PACKAGE_NAME
|
|
||||||
testApplicationId 'org.mozilla.roboexample.test'
|
|
||||||
testInstrumentationRunner 'org.mozilla.gecko.FennecInstrumentationTestRunner'
|
|
||||||
manifestPlaceholders = [
|
|
||||||
ANDROID_PACKAGE_NAME: mozconfig.substs.ANDROID_PACKAGE_NAME,
|
|
||||||
MOZ_ANDROID_MIN_SDK_VERSION: mozconfig.substs.MOZ_ANDROID_MIN_SDK_VERSION,
|
|
||||||
MOZ_ANDROID_SHARED_ID: "${mozconfig.substs.ANDROID_PACKAGE_NAME}.sharedID",
|
|
||||||
]
|
|
||||||
// Used by Robolectric based tests; see TestRunner.
|
|
||||||
buildConfigField 'String', 'BUILD_DIR', "\"${project.buildDir}\""
|
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_7
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_7
|
|
||||||
}
|
|
||||||
|
|
||||||
dexOptions {
|
|
||||||
javaMaxHeapSize "2g"
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
abortOnError true
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
shrinkResources true
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFile "${topsrcdir}/mobile/android/config/proguard/proguard.cfg"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
// For API 21+ - with multi dex, this will be faster for local development.
|
|
||||||
local {
|
|
||||||
// For multi dex, setting `minSdkVersion 21` allows the Android gradle plugin to
|
|
||||||
// pre-DEX each module and produce an APK that can be tested on
|
|
||||||
// Android Lollipop without time consuming DEX merging processes.
|
|
||||||
minSdkVersion 21
|
|
||||||
dexOptions {
|
|
||||||
preDexLibraries true
|
|
||||||
// We only call `MultiDex.install()` for the automation build flavor
|
|
||||||
// so this may not work. However, I don't think the multidex support
|
|
||||||
// library is necessary for 21+, so I expect that it will work.
|
|
||||||
multiDexEnabled true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For API < 21 - does not support multi dex because local development
|
|
||||||
// is slow in that case. Most builds will not require multi dex so this
|
|
||||||
// should not be an issue.
|
|
||||||
localOld {
|
|
||||||
}
|
|
||||||
// Automation builds.
|
|
||||||
automation {
|
|
||||||
dexOptions {
|
|
||||||
// As of FF48 on beta, the "test", "lint", etc. treeherder jobs fail because they
|
|
||||||
// exceed the method limit. Beta includes Adjust and its GPS dependencies, which
|
|
||||||
// increase the method count & explain the failures. Furthermore, this error only
|
|
||||||
// occurs on debug builds because we don't proguard.
|
|
||||||
//
|
|
||||||
// We enable multidex as an easy, quick-fix with minimal side effects but before we
|
|
||||||
// move to gradle for our production builds, we should re-evaluate this decision
|
|
||||||
// (bug 1286677).
|
|
||||||
multiDexEnabled true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
manifest.srcFile "${project.buildDir}/generated/source/preprocessed_manifest/AndroidManifest.xml"
|
|
||||||
|
|
||||||
aidl {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/base/aidl"
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/base/java"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/search/java"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/javaaddons/java"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/services/src/main/java"
|
|
||||||
|
|
||||||
if (mozconfig.substs.MOZ_ANDROID_MLS_STUMBLER) {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/stumbler/java"
|
|
||||||
}
|
|
||||||
|
|
||||||
exclude 'org/mozilla/gecko/CrashReporter.java'
|
|
||||||
|
|
||||||
if (!mozconfig.substs.MOZ_NATIVE_DEVICES) {
|
|
||||||
exclude 'org/mozilla/gecko/ChromeCastDisplay.java'
|
|
||||||
exclude 'org/mozilla/gecko/ChromeCastPlayer.java'
|
|
||||||
exclude 'org/mozilla/gecko/GeckoMediaPlayer.java'
|
|
||||||
exclude 'org/mozilla/gecko/GeckoPresentationDisplay.java'
|
|
||||||
exclude 'org/mozilla/gecko/MediaPlayerManager.java'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mozconfig.substs.MOZ_WEBRTC) {
|
|
||||||
srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
|
|
||||||
srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
|
|
||||||
srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
|
|
||||||
exclude 'org/mozilla/gecko/adjust/StubAdjustHelper.java'
|
|
||||||
} else {
|
|
||||||
exclude 'org/mozilla/gecko/adjust/AdjustHelper.java'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mozconfig.substs.MOZ_ANDROID_GCM) {
|
|
||||||
exclude 'org/mozilla/gecko/gcm/**/*.java'
|
|
||||||
exclude 'org/mozilla/gecko/push/**/*.java'
|
|
||||||
}
|
|
||||||
|
|
||||||
srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
|
|
||||||
}
|
|
||||||
|
|
||||||
res {
|
|
||||||
srcDir "${topsrcdir}/${mozconfig.substs.MOZ_BRANDING_DIRECTORY}/res"
|
|
||||||
srcDir "${project.buildDir}/generated/source/preprocessed_resources" // See syncPreprocessedResources.
|
|
||||||
srcDir "${topsrcdir}/mobile/android/base/resources"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/services/src/main/res"
|
|
||||||
if (mozconfig.substs.MOZ_CRASHREPORTER) {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/base/crashreporter/res"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assets {
|
|
||||||
if (mozconfig.substs.MOZ_ANDROID_DISTRIBUTION_DIRECTORY && !mozconfig.substs.MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER) {
|
|
||||||
// If we are packaging the bouncer, it will have the distribution, so don't put
|
|
||||||
// it in the main APK as well.
|
|
||||||
srcDir "${mozconfig.substs.MOZ_ANDROID_DISTRIBUTION_DIRECTORY}/assets"
|
|
||||||
}
|
|
||||||
srcDir "${topsrcdir}/mobile/android/app/assets"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
java {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/tests/background/junit4/src"
|
|
||||||
|
|
||||||
if (!mozconfig.substs.MOZ_ANDROID_GCM) {
|
|
||||||
exclude 'org/mozilla/gecko/gcm/**/*.java'
|
|
||||||
exclude 'org/mozilla/gecko/push/**/*.java'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resources {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/tests/background/junit4/resources"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
androidTest {
|
|
||||||
java {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/tests/browser/robocop/src"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/tests/background/junit3/src"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/tests/browser/junit3/src"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/tests/javaddons/src"
|
|
||||||
}
|
|
||||||
res {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/tests/browser/robocop/res"
|
|
||||||
}
|
|
||||||
assets {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/tests/browser/robocop/assets"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests.all {
|
|
||||||
// We'd like to use (Runtime.runtime.availableProcessors()/2), but
|
|
||||||
// we have tests that start test servers and the bound ports
|
|
||||||
// collide. We'll fix this soon to have much faster test cycles.
|
|
||||||
maxParallelForks 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compile 'com.android.support:multidex:1.0.0'
|
|
||||||
|
|
||||||
compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
|
||||||
compile "com.android.support:appcompat-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
|
||||||
compile "com.android.support:cardview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
|
||||||
compile "com.android.support:recyclerview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
|
||||||
compile "com.android.support:design:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
|
||||||
compile "com.android.support:customtabs:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
|
||||||
compile "com.android.support:palette-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
|
||||||
|
|
||||||
if (mozconfig.substs.MOZ_NATIVE_DEVICES) {
|
|
||||||
compile "com.android.support:mediarouter-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
|
||||||
compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
compile "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
compile "com.google.android.gms:play-services-cast:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
|
|
||||||
compile "com.google.android.gms:play-services-ads:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mozconfig.substs.MOZ_ANDROID_GCM) {
|
|
||||||
compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
compile "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
compile "com.google.android.gms:play-services-gcm:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
compile "com.google.android.gms:play-services-measurement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include LeakCanary in most gradle based builds. LeakCanary adds about 5k methods, so we disable
|
|
||||||
// it for the (non-proguarded, non-multidex) localOld builds to allow space for other libraries.
|
|
||||||
// Gradle based tests include the no-op version. Mach based builds only include the no-op version
|
|
||||||
// of this library.
|
|
||||||
// It doesn't seem like there is a non-trivial way to be conditional on 'localOld', so instead we explicitly
|
|
||||||
// define a version of leakcanary for every flavor:
|
|
||||||
localCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
|
|
||||||
localOldCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
|
|
||||||
automationCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
|
|
||||||
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
|
|
||||||
|
|
||||||
compile project(':geckoview')
|
|
||||||
compile project(':thirdparty')
|
|
||||||
|
|
||||||
testCompile 'junit:junit:4.12'
|
|
||||||
testCompile 'org.robolectric:robolectric:3.1.2'
|
|
||||||
testCompile 'org.simpleframework:simple-http:6.0.1'
|
|
||||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
|
||||||
|
|
||||||
// Including the Robotium JAR directly can cause issues with dexing.
|
|
||||||
androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.5.4'
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: (bug 1261486): This impl is not robust -
|
|
||||||
// we just wanted to land something.
|
|
||||||
task checkstyle(type: Checkstyle) {
|
|
||||||
configFile file("checkstyle.xml")
|
|
||||||
// TODO: should use sourceSets from project instead of hard-coded str.
|
|
||||||
source '../base/java/'
|
|
||||||
// TODO: This ignores our pre-processed resources.
|
|
||||||
include '**/*.java'
|
|
||||||
// TODO: classpath should probably be something.
|
|
||||||
classpath = files()
|
|
||||||
}
|
|
||||||
|
|
||||||
task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
|
|
||||||
into("${project.buildDir}/generated/source/preprocessed_code")
|
|
||||||
from("${topobjdir}/mobile/android/base/generated/preprocessed") {
|
|
||||||
// All other preprocessed code is included in the geckoview project.
|
|
||||||
include '**/AdjustConstants.java'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The localization system uses the moz.build preprocessor to interpolate a .dtd
|
|
||||||
// file of XML entity definitions into an XML file of elements referencing those
|
|
||||||
// entities. (Each locale produces its own .dtd file, backstopped by the en-US
|
|
||||||
// .dtd file in tree.) Android Studio (and IntelliJ) don't handle these inline
|
|
||||||
// entities smoothly. This filter merely expands the entities in place, making
|
|
||||||
// them appear properly throughout the IDE. Be aware that this assumes that the
|
|
||||||
// JVM's file.encoding is utf-8. See comments in
|
|
||||||
// mobile/android/mach_commands.py.
|
|
||||||
class ExpandXMLEntitiesFilter extends FilterReader {
|
|
||||||
ExpandXMLEntitiesFilter(Reader input) {
|
|
||||||
// Extremely inefficient, but whatever.
|
|
||||||
super(new StringReader(groovy.xml.XmlUtil.serialize(new XmlParser(false, false, true).parse(input))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task syncPreprocessedResources(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
|
|
||||||
into("${project.buildDir}/generated/source/preprocessed_resources")
|
|
||||||
from("${topobjdir}/mobile/android/base/res")
|
|
||||||
filesMatching('**/strings.xml') {
|
|
||||||
filter(ExpandXMLEntitiesFilter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's not easy -- see the backout in Bug 1242213 -- to change the <manifest>
|
|
||||||
// package for Fennec. Gradle has grown a mechanism to achieve what we want for
|
|
||||||
// Fennec, however, with applicationId. To use the same manifest as moz.build,
|
|
||||||
// we replace the package with org.mozilla.gecko (the eventual package) here.
|
|
||||||
task rewriteManifestPackage(type: Copy, dependsOn: rootProject.generateCodeAndResources) {
|
|
||||||
into("${project.buildDir}/generated/source/preprocessed_manifest")
|
|
||||||
from("${topobjdir}/mobile/android/base/AndroidManifest.xml")
|
|
||||||
filter { it.replaceFirst(/package=".*?"/, 'package="org.mozilla.gecko"') }
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
|
|
||||||
|
|
||||||
android.applicationVariants.all { variant ->
|
|
||||||
variant.preBuild.dependsOn rewriteManifestPackage
|
|
||||||
variant.preBuild.dependsOn syncPreprocessedCode
|
|
||||||
variant.preBuild.dependsOn syncPreprocessedResources
|
|
||||||
|
|
||||||
// Automation builds don't include Gecko binaries, since those binaries are
|
|
||||||
// not produced until after build time (at package time). Therefore,
|
|
||||||
// automation builds include the Gecko binaries into the APK at package
|
|
||||||
// time. The "withGeckoBinaries" variant of the :geckoview project also
|
|
||||||
// does this. (It does what it says on the tin!) For notes on this
|
|
||||||
// approach, see mobile/android/gradle/with_gecko_binaries.gradle.
|
|
||||||
|
|
||||||
// Like 'local' or 'localOld'.
|
|
||||||
def productFlavor = variant.productFlavors[0].name
|
|
||||||
|
|
||||||
// :app uses :geckoview:release and handles it's own Gecko binary inclusion,
|
|
||||||
// even though this would be most naturally done in the :geckoview project.
|
|
||||||
if (!productFlavor.equals('automation')) {
|
|
||||||
configureVariantWithGeckoBinaries(variant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'spoon'
|
|
||||||
|
|
||||||
spoon {
|
|
||||||
// For now, let's be verbose.
|
|
||||||
debug = true
|
|
||||||
// It's not helpful to pass when we don't have a device connected.
|
|
||||||
failIfNoDeviceConnected = true
|
|
||||||
|
|
||||||
def spoonPackageName
|
|
||||||
if (gradle.startParameter.taskNames.contains('runBrowserTests')) {
|
|
||||||
spoonPackageName = 'org.mozilla.tests.browser.junit3'
|
|
||||||
}
|
|
||||||
if (gradle.startParameter.taskNames.contains('runBackgroundTests')) {
|
|
||||||
spoonPackageName = 'org.mozilla.gecko.background'
|
|
||||||
}
|
|
||||||
if (project.hasProperty('spoonPackageName')) {
|
|
||||||
// Command line overrides everything.
|
|
||||||
spoonPackageName = project.spoonPackageName
|
|
||||||
}
|
|
||||||
if (spoonPackageName) {
|
|
||||||
instrumentationArgs = ['-e', "package=${spoonPackageName}".toString()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// See discussion at https://github.com/stanfy/spoon-gradle-plugin/issues/9.
|
|
||||||
afterEvaluate {
|
|
||||||
tasks["spoonLocal${android.testBuildType.capitalize()}AndroidTest"].outputs.upToDateWhen { false }
|
|
||||||
|
|
||||||
// This is an awkward way to define different sets of instrumentation tests.
|
|
||||||
// The task name itself is fished at runtime and the package name configured
|
|
||||||
// in the spoon configuration.
|
|
||||||
task runBrowserTests {
|
|
||||||
dependsOn tasks["spoonLocalOldDebugAndroidTest"]
|
|
||||||
}
|
|
||||||
task runBackgroundTests {
|
|
||||||
dependsOn tasks["spoonLocalOldDebugAndroidTest"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bug 1299015: Complain to treeherder if checkstyle, lint, or unittest fails. It's not obvious
|
|
||||||
// how to listen to individual errors in most cases, so we just link to the reports for now.
|
|
||||||
def makeTaskExecutionListener(artifactRootUrl) {
|
|
||||||
return new TaskExecutionListener() {
|
|
||||||
void beforeExecute(Task task) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
void afterExecute(Task task, TaskState state) {
|
|
||||||
if (!state.failure) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Link to the failing report. The task path and the report path
|
|
||||||
// depend on the android-lint task in
|
|
||||||
// taskcluster/ci/android-stuff/kind.yml. It's not possible to link
|
|
||||||
// directly, so for now consumers will need to copy-paste the URL.
|
|
||||||
switch (task.path) {
|
|
||||||
case ':app:checkstyle':
|
|
||||||
def url = "${artifactRootUrl}/public/android/checkstyle/checkstyle.xml"
|
|
||||||
println "TEST-UNEXPECTED-FAIL | android-checkstyle | Checkstyle rule violations were found. See the report at: $url"
|
|
||||||
break
|
|
||||||
|
|
||||||
case ':app:lintAutomationDebug':
|
|
||||||
def url = "${artifactRootUrl}/public/android/lint/lint-results-automationDebug.html"
|
|
||||||
println "TEST-UNEXPECTED-FAIL | android-lint | Lint found errors in the project; aborting build. See the report at: $url"
|
|
||||||
break
|
|
||||||
|
|
||||||
case ':app:testAutomationDebugUnitTest':
|
|
||||||
def url = "${artifactRootUrl}/public/android/unittest/automationDebug/index.html"
|
|
||||||
println "TEST-UNEXPECTED-FAIL | android-test | There were failing tests. See the report at: $url"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TASK_ID and RUN_ID are provided by docker-worker; see
|
|
||||||
// https://docs.taskcluster.net/manual/execution/workers/docker-worker.
|
|
||||||
if (System.env.TASK_ID && System.env.RUN_ID) {
|
|
||||||
def artifactRootUrl = "https://queue.taskcluster.net/v1/task/${System.env.TASK_ID}/runs/${System.env.RUN_ID}/artifacts"
|
|
||||||
gradle.addListener(makeTaskExecutionListener(artifactRootUrl))
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<!-- 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/. -->
|
|
||||||
<!DOCTYPE module PUBLIC
|
|
||||||
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
|
|
||||||
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
|
|
||||||
|
|
||||||
<!-- TODO: Clean up code & add checks:
|
|
||||||
- WhitespaceAround
|
|
||||||
- EmptyLineSeparator
|
|
||||||
- NeedBraces
|
|
||||||
- LeftCurly // placement of "{" in new scope or literal
|
|
||||||
- RightCurly // placement of "}" in close scope or literal
|
|
||||||
- Indentation
|
|
||||||
- OneStatementPerLine
|
|
||||||
- OperatorWrap
|
|
||||||
- SeparatorWrap
|
|
||||||
- MultipleVariableDeclarations
|
|
||||||
- FallThrough
|
|
||||||
(I spent too much time already)
|
|
||||||
|
|
||||||
Maybe add:
|
|
||||||
- OneTopLevelClass
|
|
||||||
- OverloadMethodsDeclarationOrder
|
|
||||||
- Empty*Block // better to catch errors!
|
|
||||||
(I spent too much time already)
|
|
||||||
|
|
||||||
See http://checkstyle.sourceforge.net/google_style.html
|
|
||||||
for a good set of defaults.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<module name="Checker">
|
|
||||||
<property name="charset" value="UTF-8"/>
|
|
||||||
|
|
||||||
<!-- TODO: <property name="fileExtensions" value="java, properties, xml"/> -->
|
|
||||||
|
|
||||||
<module name="FileTabCharacter"> <!-- No tabs! -->
|
|
||||||
<property name="eachLine" value="true"/>
|
|
||||||
</module>
|
|
||||||
<module name="RegexpSingleline"> <!-- excess whitespace -->
|
|
||||||
<property name="format" value="\s+$"/>
|
|
||||||
<property name="message" value="Trailing whitespace"/>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
<module name="TreeWalker">
|
|
||||||
<module name="GenericWhitespace"/> <!-- whitespace for generics -->
|
|
||||||
<module name="NoLineWrap">
|
|
||||||
<property name="tokens" value="IMPORT,PACKAGE_DEF"/>
|
|
||||||
</module>
|
|
||||||
<module name="OuterTypeFilename"/> <!-- `class Lol` only in Lol.java -->
|
|
||||||
<module name="WhitespaceAfter">
|
|
||||||
<!-- TODO: (bug 1263059) Remove specific tokens to enable CAST check. -->
|
|
||||||
<property name="tokens" value="COMMA, SEMI"/>
|
|
||||||
</module>
|
|
||||||
<module name="WhitespaceAround">
|
|
||||||
<property name="allowEmptyConstructors" value="true"/>
|
|
||||||
<property name="allowEmptyMethods" value="true"/>
|
|
||||||
<property name="allowEmptyTypes" value="true"/>
|
|
||||||
</module>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
</module>
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<lint>
|
|
||||||
<!-- Enable relevant checks disabled by default -->
|
|
||||||
<issue id="NegativeMargin" severity="warning" />
|
|
||||||
|
|
||||||
<!-- We have a custom menu and don't conform to the recommended styles. -->
|
|
||||||
<issue id="IconColors" severity="ignore" />
|
|
||||||
|
|
||||||
<!-- We use our own preprocessing to either add or remove
|
|
||||||
`android:debuggable` when building with mach so it's
|
|
||||||
not actually hard-coded. We can probably remove this
|
|
||||||
warning when we switch to gradle. -->
|
|
||||||
<issue id="HardcodedDebugMode" severity="ignore" />
|
|
||||||
|
|
||||||
<!-- We have our own l10n system & don't use the platform's plurals. -->
|
|
||||||
<issue id="PluralsCandidate" severity="ignore" />
|
|
||||||
|
|
||||||
<!-- We don't want to have to follow the SDK release schedule: we can keep
|
|
||||||
the warning in order to not forget that there's a new SDK, but there's
|
|
||||||
no need to break on update. -->
|
|
||||||
<issue id="OldTargetApi" severity="warning" />
|
|
||||||
|
|
||||||
<!-- We want all lint warnings to be fatal errors.
|
|
||||||
Right now, we set these to lint warnings so:
|
|
||||||
|
|
||||||
DO NOT ADD TO THIS LIST.
|
|
||||||
|
|
||||||
We did this so we can land lint in automation
|
|
||||||
and not fail everything. -->
|
|
||||||
<issue id="AppCompatResource" severity="warning" />
|
|
||||||
<issue id="GoogleAppIndexingDeepLinkError" severity="warning" />
|
|
||||||
<issue id="GoogleAppIndexingUrlError" severity="warning" />
|
|
||||||
<issue id="Instantiatable" severity="warning" />
|
|
||||||
<issue id="LongLogTag" severity="warning" />
|
|
||||||
<issue id="MissingPermission" severity="warning" />
|
|
||||||
<issue id="NewApi" severity="warning" />
|
|
||||||
<issue id="OnClick" severity="warning" />
|
|
||||||
<issue id="ReferenceType" severity="warning" />
|
|
||||||
<issue id="ResourceAsColor" severity="warning" />
|
|
||||||
<issue id="ResourceType" severity="warning" />
|
|
||||||
<issue id="ValidFragment" severity="warning" />
|
|
||||||
<issue id="WrongConstant" severity="warning" />
|
|
||||||
|
|
||||||
<!-- We fixed all "Registered" lint errors. However the current gradle plugin has a bug where
|
|
||||||
it ignores @SuppressLint annotations for this check. See CrashReporter class and
|
|
||||||
https://code.google.com/p/android/issues/detail?id=204846 -->
|
|
||||||
<issue id="Registered" severity="warning" />
|
|
||||||
|
|
||||||
<!-- WHEN YOU FIX A LINT WARNING, ADD IT TO THIS LIST.
|
|
||||||
|
|
||||||
We want all lint warnings to be fatal errors.
|
|
||||||
This is the list of checks that we've explicitly
|
|
||||||
set as errors. Ideally, once we have no more warnings,
|
|
||||||
we switch to the `warningsAsErrors` lint option
|
|
||||||
(bug 1253737) rather than listing everything explicitly. -->
|
|
||||||
<issue id="AaptCrash" severity="error" />
|
|
||||||
<issue id="Accessibility" severity="error" />
|
|
||||||
<issue id="AccidentalOctal" severity="error" />
|
|
||||||
<issue id="AdapterViewChildren" severity="error" />
|
|
||||||
<issue id="AddJavascriptInterface" severity="error" />
|
|
||||||
<issue id="AllowBackup" severity="error" />
|
|
||||||
<issue id="AlwaysShowAction" severity="error" />
|
|
||||||
<issue id="AndroidGradlePluginVersion" severity="error" />
|
|
||||||
<issue id="AppCompatMethod" severity="error" />
|
|
||||||
<issue id="AppIndexingError" severity="error" />
|
|
||||||
<issue id="AppIndexingWarning" severity="error" />
|
|
||||||
<issue id="Assert" severity="error" />
|
|
||||||
<issue id="ButtonCase" severity="error" />
|
|
||||||
<issue id="ButtonOrder" severity="error" />
|
|
||||||
<issue id="ByteOrderMark" severity="error" />
|
|
||||||
<issue id="CheckResult" severity="error" />
|
|
||||||
<issue id="Correctness" severity="error" />
|
|
||||||
<issue id="CutPasteId" severity="error" />
|
|
||||||
<issue id="DalvikOverride" severity="error" />
|
|
||||||
<issue id="DeviceAdmin" severity="error" />
|
|
||||||
<issue id="DisableBaselineAlignment" severity="error" />
|
|
||||||
<issue id="DrawAllocation" severity="error" />
|
|
||||||
<issue id="DuplicateActivity" severity="error" />
|
|
||||||
<issue id="DuplicateDefinition" severity="error" />
|
|
||||||
<issue id="DuplicateIds" severity="error" />
|
|
||||||
<issue id="DuplicateIncludedIds" severity="error" />
|
|
||||||
<issue id="DuplicateUsesFeature" severity="error" />
|
|
||||||
<issue id="ExportedContentProvider" severity="error" />
|
|
||||||
<issue id="ExportedPreferenceActivity" severity="error" />
|
|
||||||
<issue id="ExtraText" severity="error" />
|
|
||||||
<issue id="ExtraTranslation" severity="error" />
|
|
||||||
<issue id="FloatMath" severity="error" />
|
|
||||||
<issue id="FullBackupContent" severity="error" />
|
|
||||||
<issue id="GetInstance" severity="error" />
|
|
||||||
<issue id="GifUsage" severity="error" />
|
|
||||||
<issue id="GradleCompatible" severity="error" />
|
|
||||||
<issue id="GradleDependency" severity="error" />
|
|
||||||
<issue id="GradleDeprecated" severity="error" />
|
|
||||||
<issue id="GradleDynamicVersion" severity="error" />
|
|
||||||
<issue id="GradleGetter" severity="error" />
|
|
||||||
<issue id="GradleIdeError" severity="error" />
|
|
||||||
<issue id="GradlePath" severity="error" />
|
|
||||||
<issue id="GrantAllUris" severity="error" />
|
|
||||||
<issue id="GridLayout" severity="error" />
|
|
||||||
<issue id="HandlerLeak" severity="error" />
|
|
||||||
<issue id="HardcodedText" severity="error" />
|
|
||||||
<issue id="IconExtension" severity="error" />
|
|
||||||
<issue id="IconLauncherShape" severity="error" />
|
|
||||||
<issue id="IconMixedNinePatch" severity="error" />
|
|
||||||
<issue id="IconNoDpi" severity="error" />
|
|
||||||
<issue id="IllegalResourceRef" severity="error" />
|
|
||||||
<issue id="ImpliedQuantity" severity="error" />
|
|
||||||
<issue id="InOrMmUsage" severity="error" />
|
|
||||||
<issue id="IncludeLayoutParam" severity="error" />
|
|
||||||
<issue id="InconsistentArrays" severity="error" />
|
|
||||||
<issue id="InefficientWeight" severity="error" />
|
|
||||||
<issue id="InnerclassSeparator" severity="error" />
|
|
||||||
<issue id="Internationalization" severity="error" />
|
|
||||||
<issue id="InvalidId" severity="error" />
|
|
||||||
<issue id="InvalidPackage" severity="error" />
|
|
||||||
<issue id="InvalidResourceFolder" severity="error" />
|
|
||||||
<issue id="JavascriptInterface" severity="error" />
|
|
||||||
<issue id="LabelFor" severity="error" />
|
|
||||||
<issue id="LibraryCustomView" severity="error" />
|
|
||||||
<issue id="LocalSuppress" severity="error" />
|
|
||||||
<issue id="LocaleFolder" severity="error" />
|
|
||||||
<issue id="LogTagMismatch" severity="error" />
|
|
||||||
<issue id="MangledCRLF" severity="error" />
|
|
||||||
<issue id="ManifestOrder" severity="error" />
|
|
||||||
<issue id="ManifestTypo" severity="error" />
|
|
||||||
<issue id="MenuTitle" severity="error" />
|
|
||||||
<issue id="MergeRootFrame" severity="error" />
|
|
||||||
<issue id="MipmapIcons" severity="error" />
|
|
||||||
<issue id="MissingApplicationIcon" severity="error" />
|
|
||||||
<issue id="MissingId" severity="error" />
|
|
||||||
<issue id="MissingPrefix" severity="error" />
|
|
||||||
<issue id="MissingQuantity" severity="error" />
|
|
||||||
<issue id="MissingRegistered" severity="error" />
|
|
||||||
<issue id="MissingSuperCall" severity="error" />
|
|
||||||
<issue id="MissingTranslation" severity="error" />
|
|
||||||
<issue id="MissingVersion" severity="error" />
|
|
||||||
<issue id="MockLocation" severity="error" />
|
|
||||||
<issue id="MultipleUsesSdk" severity="error" />
|
|
||||||
<issue id="NamespaceTypo" severity="error" />
|
|
||||||
<issue id="NestedScrolling" severity="error" />
|
|
||||||
<issue id="NfcTechWhitespace" severity="error" />
|
|
||||||
<issue id="NotSibling" severity="error" />
|
|
||||||
<issue id="ObsoleteLayoutParam" severity="error" />
|
|
||||||
<issue id="OnClick" severity="error" />
|
|
||||||
<issue id="Orientation" severity="error" />
|
|
||||||
<issue id="Override" severity="error" />
|
|
||||||
<issue id="OverrideAbstract" severity="error" />
|
|
||||||
<issue id="PackagedPrivateKey" severity="error" />
|
|
||||||
<issue id="ParcelCreator" severity="error" />
|
|
||||||
<issue id="Performance" severity="error" />
|
|
||||||
<issue id="Proguard" severity="error" />
|
|
||||||
<issue id="ProguardSplit" severity="error" />
|
|
||||||
<issue id="PropertyEscape" severity="error" />
|
|
||||||
<issue id="ProtectedPermissions" severity="error" />
|
|
||||||
<issue id="PxUsage" severity="error" />
|
|
||||||
<issue id="Range" severity="error" />
|
|
||||||
<issue id="RelativeOverlap" severity="error" />
|
|
||||||
<issue id="RequiredSize" severity="error" />
|
|
||||||
<issue id="ResAuto" severity="error" />
|
|
||||||
<issue id="ResourceCycle" severity="error" />
|
|
||||||
<issue id="ResourceName" severity="error" />
|
|
||||||
<issue id="ResourceType" severity="error" />
|
|
||||||
<issue id="RtlCompat" severity="error" />
|
|
||||||
<issue id="RtlEnabled" severity="error" />
|
|
||||||
<issue id="ScrollViewCount" severity="error" />
|
|
||||||
<issue id="ScrollViewSize" severity="error" />
|
|
||||||
<issue id="SecureRandom" severity="error" />
|
|
||||||
<issue id="Security" severity="error" />
|
|
||||||
<issue id="ServiceCast" severity="error" />
|
|
||||||
<issue id="SetJavaScriptEnabled" severity="error" />
|
|
||||||
<issue id="ShiftFlags" severity="error" />
|
|
||||||
<issue id="ShortAlarm" severity="error" />
|
|
||||||
<issue id="ShowToast" severity="error" />
|
|
||||||
<issue id="SignatureOrSystemPermissions" severity="error" />
|
|
||||||
<issue id="StringFormatCount" severity="error" />
|
|
||||||
<issue id="StringFormatInvalid" severity="error" />
|
|
||||||
<issue id="StringFormatMatches" severity="error" />
|
|
||||||
<issue id="StringShouldBeInt" severity="error" />
|
|
||||||
<issue id="SuspiciousImport" severity="error" />
|
|
||||||
<issue id="TextFields" severity="error" />
|
|
||||||
<issue id="TextViewEdits" severity="error" />
|
|
||||||
<issue id="TooDeepLayout" severity="error" />
|
|
||||||
<issue id="TooManyViews" severity="error" />
|
|
||||||
<issue id="TrulyRandom" severity="error" />
|
|
||||||
<issue id="TypographyDashes" severity="error" />
|
|
||||||
<issue id="TypographyFractions" severity="error" />
|
|
||||||
<issue id="TypographyOther" severity="error" />
|
|
||||||
<issue id="Typos" severity="error" />
|
|
||||||
<issue id="UniqueConstants" severity="error" />
|
|
||||||
<issue id="UniquePermission" severity="error" />
|
|
||||||
<issue id="UnknownId" severity="error" />
|
|
||||||
<issue id="UnknownIdInLayout" severity="error" />
|
|
||||||
<issue id="UnlocalizedSms" severity="error" />
|
|
||||||
<issue id="UnusedNamespace" severity="error" />
|
|
||||||
<issue id="UnusedQuantity" severity="error" />
|
|
||||||
<issue id="UnusedResources" severity="error">
|
|
||||||
<!-- The moz.build based build system leaves a .mkdir.done file lying around in the
|
|
||||||
preprocessed_resources res/raw folder. Lint reports it as unused. We should get
|
|
||||||
rid of the file eventually. See bug 1268948. -->
|
|
||||||
<ignore path="**/raw/.mkdir.done" />
|
|
||||||
</issue>
|
|
||||||
<issue id="Usability" severity="error" />
|
|
||||||
<issue id="UseCheckPermission" severity="error" />
|
|
||||||
<issue id="UseCompoundDrawables" severity="error" />
|
|
||||||
<issue id="UselessLeaf" severity="error" />
|
|
||||||
<issue id="UsesMinSdkAttributes" severity="error" />
|
|
||||||
<issue id="UsingHttp" severity="error" />
|
|
||||||
<issue id="ViewHolder" severity="error" />
|
|
||||||
<issue id="ViewTag" severity="error" />
|
|
||||||
<issue id="Wakelock" severity="error" />
|
|
||||||
<issue id="WebViewLayout" severity="error" />
|
|
||||||
<issue id="WorldReadableFiles" severity="error" />
|
|
||||||
<issue id="WorldWriteableFiles" severity="error" />
|
|
||||||
<issue id="WrongCall" severity="error" />
|
|
||||||
<issue id="WrongCase" severity="error" />
|
|
||||||
<issue id="WrongConstant" severity="error" />
|
|
||||||
<issue id="WrongFolder" severity="error" />
|
|
||||||
<issue id="WrongManifestParent" severity="error" />
|
|
||||||
<issue id="WrongRegion" severity="error" />
|
|
||||||
<issue id="WrongThread" severity="error" />
|
|
||||||
<issue id="WrongViewCast" severity="error" />
|
|
||||||
|
|
||||||
</lint>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
|
|
@ -1,916 +0,0 @@
|
||||||
/* 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/. */
|
|
||||||
|
|
||||||
#filter substitution
|
|
||||||
|
|
||||||
// For browser.xml binding
|
|
||||||
//
|
|
||||||
// cacheRatio* is a ratio that determines the amount of pixels to cache. The
|
|
||||||
// ratio is multiplied by the viewport width or height to get the displayport's
|
|
||||||
// width or height, respectively.
|
|
||||||
//
|
|
||||||
// (divide integer value by 1000 to get the ratio)
|
|
||||||
//
|
|
||||||
// For instance: cachePercentageWidth is 1500
|
|
||||||
// viewport height is 500
|
|
||||||
// => display port height will be 500 * 1.5 = 750
|
|
||||||
//
|
|
||||||
pref("toolkit.browser.cacheRatioWidth", 2000);
|
|
||||||
pref("toolkit.browser.cacheRatioHeight", 3000);
|
|
||||||
|
|
||||||
// How long before a content view (a handle to a remote scrollable object)
|
|
||||||
// expires.
|
|
||||||
pref("toolkit.browser.contentViewExpire", 3000);
|
|
||||||
|
|
||||||
pref("toolkit.defaultChromeURI", "chrome://browser/content/browser.xul");
|
|
||||||
pref("browser.chromeURL", "chrome://browser/content/");
|
|
||||||
|
|
||||||
// If a tab has not been active for this long (seconds), then it may be
|
|
||||||
// turned into a zombie tab to preemptively free up memory. -1 disables time-based
|
|
||||||
// expiration (but low-memory conditions may still require the tab to be zombified).
|
|
||||||
pref("browser.tabs.expireTime", 900);
|
|
||||||
|
|
||||||
// Disables zombification of background tabs under memory pressure.
|
|
||||||
// Intended for use in testing, where we don't want the tab running the
|
|
||||||
// test harness code to be zombified.
|
|
||||||
pref("browser.tabs.disableBackgroundZombification", false);
|
|
||||||
|
|
||||||
// Control whether tab content should try to load from disk cache when network
|
|
||||||
// is offline.
|
|
||||||
// Controlled by Switchboard experiment "offline-cache".
|
|
||||||
pref("browser.tabs.useCache", false);
|
|
||||||
|
|
||||||
// From libpref/src/init/all.js, extended to allow a slightly wider zoom range.
|
|
||||||
pref("zoom.minPercent", 20);
|
|
||||||
pref("zoom.maxPercent", 400);
|
|
||||||
pref("toolkit.zoomManager.zoomValues", ".2,.3,.5,.67,.8,.9,1,1.1,1.2,1.33,1.5,1.7,2,2.4,3,4");
|
|
||||||
|
|
||||||
// Mobile will use faster, less durable mode.
|
|
||||||
pref("toolkit.storage.synchronous", 0);
|
|
||||||
|
|
||||||
pref("browser.viewport.desktopWidth", 980);
|
|
||||||
// The default fallback zoom level to render pages at. Set to -1 to fit page; otherwise
|
|
||||||
// the value is divided by 1000 and clamped to hard-coded min/max scale values.
|
|
||||||
pref("browser.viewport.defaultZoom", -1);
|
|
||||||
|
|
||||||
// Show/Hide scrollbars when active/inactive
|
|
||||||
pref("ui.showHideScrollbars", 1);
|
|
||||||
pref("ui.useOverlayScrollbars", 1);
|
|
||||||
pref("ui.scrollbarFadeBeginDelay", 450);
|
|
||||||
pref("ui.scrollbarFadeDuration", 0);
|
|
||||||
|
|
||||||
/* turn off the caret blink after 10 cycles */
|
|
||||||
pref("ui.caretBlinkCount", 10);
|
|
||||||
|
|
||||||
/* cache prefs */
|
|
||||||
pref("browser.cache.disk.enable", true);
|
|
||||||
pref("browser.cache.disk.capacity", 20480); // kilobytes
|
|
||||||
pref("browser.cache.disk.max_entry_size", 4096); // kilobytes
|
|
||||||
pref("browser.cache.disk.smart_size.enabled", true);
|
|
||||||
pref("browser.cache.disk.smart_size.first_run", true);
|
|
||||||
|
|
||||||
#ifdef MOZ_PKG_SPECIAL
|
|
||||||
// low memory devices
|
|
||||||
pref("browser.cache.memory.enable", false);
|
|
||||||
#else
|
|
||||||
pref("browser.cache.memory.enable", true);
|
|
||||||
#endif
|
|
||||||
pref("browser.cache.memory.capacity", 1024); // kilobytes
|
|
||||||
|
|
||||||
pref("browser.cache.memory_limit", 5120); // 5 MB
|
|
||||||
|
|
||||||
/* image cache prefs */
|
|
||||||
pref("image.cache.size", 1048576); // bytes
|
|
||||||
|
|
||||||
/* offline cache prefs */
|
|
||||||
pref("browser.offline-apps.notify", true);
|
|
||||||
pref("browser.cache.offline.enable", true);
|
|
||||||
pref("browser.cache.offline.capacity", 5120); // kilobytes
|
|
||||||
pref("offline-apps.quota.warn", 1024); // kilobytes
|
|
||||||
|
|
||||||
// cache compression turned off for now - see bug #715198
|
|
||||||
pref("browser.cache.compression_level", 0);
|
|
||||||
|
|
||||||
/* disable some protocol warnings */
|
|
||||||
pref("network.protocol-handler.warn-external.tel", false);
|
|
||||||
pref("network.protocol-handler.warn-external.sms", false);
|
|
||||||
pref("network.protocol-handler.warn-external.mailto", false);
|
|
||||||
pref("network.protocol-handler.warn-external.vnd.youtube", false);
|
|
||||||
|
|
||||||
/* http prefs */
|
|
||||||
pref("network.http.pipelining", true);
|
|
||||||
pref("network.http.pipelining.ssl", true);
|
|
||||||
pref("network.http.proxy.pipelining", true);
|
|
||||||
pref("network.http.pipelining.maxrequests" , 6);
|
|
||||||
pref("network.http.keep-alive.timeout", 109);
|
|
||||||
pref("network.http.max-connections", 20);
|
|
||||||
pref("network.http.max-persistent-connections-per-server", 6);
|
|
||||||
pref("network.http.max-persistent-connections-per-proxy", 20);
|
|
||||||
|
|
||||||
// spdy
|
|
||||||
pref("network.http.spdy.push-allowance", 32768);
|
|
||||||
pref("network.http.spdy.default-hpack-buffer", 4096); // 4k
|
|
||||||
|
|
||||||
// See bug 545869 for details on why these are set the way they are
|
|
||||||
pref("network.buffer.cache.count", 24);
|
|
||||||
pref("network.buffer.cache.size", 16384);
|
|
||||||
|
|
||||||
// predictive actions
|
|
||||||
pref("network.predictor.enabled", true);
|
|
||||||
pref("network.predictor.max-db-size", 2097152); // bytes
|
|
||||||
pref("network.predictor.preserve", 50); // percentage of predictor data to keep when cleaning up
|
|
||||||
|
|
||||||
// Use JS mDNS as a fallback
|
|
||||||
pref("network.mdns.use_js_fallback", true);
|
|
||||||
|
|
||||||
/* history max results display */
|
|
||||||
pref("browser.display.history.maxresults", 100);
|
|
||||||
|
|
||||||
/* How many times should have passed before the remote tabs list is refreshed */
|
|
||||||
pref("browser.display.remotetabs.timeout", 10);
|
|
||||||
|
|
||||||
/* session history */
|
|
||||||
pref("browser.sessionhistory.max_total_viewers", 1);
|
|
||||||
pref("browser.sessionhistory.max_entries", 50);
|
|
||||||
pref("browser.sessionhistory.contentViewerTimeout", 360);
|
|
||||||
pref("browser.sessionhistory.bfcacheIgnoreMemoryPressure", false);
|
|
||||||
|
|
||||||
/* session store */
|
|
||||||
pref("browser.sessionstore.resume_session_once", false);
|
|
||||||
pref("browser.sessionstore.resume_from_crash", true);
|
|
||||||
pref("browser.sessionstore.interval", 10000); // milliseconds
|
|
||||||
pref("browser.sessionstore.backupInterval", 120000); // milliseconds -> 2 minutes
|
|
||||||
pref("browser.sessionstore.max_tabs_undo", 10);
|
|
||||||
pref("browser.sessionstore.max_resumed_crashes", 1);
|
|
||||||
pref("browser.sessionstore.privacy_level", 0); // saving data: 0 = all, 1 = unencrypted sites, 2 = never
|
|
||||||
pref("browser.sessionstore.debug_logging", false);
|
|
||||||
|
|
||||||
/* these should help performance */
|
|
||||||
pref("mozilla.widget.force-24bpp", true);
|
|
||||||
pref("mozilla.widget.use-buffer-pixmap", true);
|
|
||||||
pref("mozilla.widget.disable-native-theme", true);
|
|
||||||
pref("layout.reflow.synthMouseMove", false);
|
|
||||||
pref("layout.css.report_errors", false);
|
|
||||||
|
|
||||||
/* download manager (don't show the window or alert) */
|
|
||||||
pref("browser.download.useDownloadDir", true);
|
|
||||||
pref("browser.download.folderList", 1); // Default to ~/Downloads
|
|
||||||
pref("browser.download.manager.showAlertOnComplete", false);
|
|
||||||
pref("browser.download.manager.showAlertInterval", 2000);
|
|
||||||
pref("browser.download.manager.retention", 2);
|
|
||||||
pref("browser.download.manager.showWhenStarting", false);
|
|
||||||
pref("browser.download.manager.closeWhenDone", true);
|
|
||||||
pref("browser.download.manager.openDelay", 0);
|
|
||||||
pref("browser.download.manager.focusWhenStarting", false);
|
|
||||||
pref("browser.download.manager.flashCount", 2);
|
|
||||||
pref("browser.download.manager.displayedHistoryDays", 7);
|
|
||||||
pref("browser.download.manager.addToRecentDocs", true);
|
|
||||||
|
|
||||||
/* download helper */
|
|
||||||
pref("browser.helperApps.deleteTempFileOnExit", false);
|
|
||||||
|
|
||||||
/* password manager */
|
|
||||||
pref("signon.rememberSignons", true);
|
|
||||||
pref("signon.autofillForms.http", true);
|
|
||||||
pref("signon.expireMasterPassword", false);
|
|
||||||
pref("signon.debug", false);
|
|
||||||
|
|
||||||
/* form helper (scroll to and optionally zoom into editable fields) */
|
|
||||||
pref("formhelper.mode", 2); // 0 = disabled, 1 = enabled, 2 = dynamic depending on screen size
|
|
||||||
pref("formhelper.autozoom", true);
|
|
||||||
|
|
||||||
/* find helper */
|
|
||||||
pref("findhelper.autozoom", true);
|
|
||||||
|
|
||||||
/* autocomplete */
|
|
||||||
pref("browser.formfill.enable", true);
|
|
||||||
|
|
||||||
/* spellcheck */
|
|
||||||
pref("layout.spellcheckDefault", 0);
|
|
||||||
|
|
||||||
/* new html5 forms */
|
|
||||||
pref("dom.experimental_forms", true);
|
|
||||||
pref("dom.forms.number", true);
|
|
||||||
|
|
||||||
/* extension manager and xpinstall */
|
|
||||||
pref("xpinstall.whitelist.directRequest", false);
|
|
||||||
pref("xpinstall.whitelist.fileRequest", false);
|
|
||||||
pref("xpinstall.whitelist.add", "https://addons.mozilla.org,https://testpilot.firefox.com");
|
|
||||||
|
|
||||||
pref("xpinstall.signatures.required", true);
|
|
||||||
|
|
||||||
pref("extensions.enabledScopes", 1);
|
|
||||||
pref("extensions.autoupdate.enabled", true);
|
|
||||||
pref("extensions.autoupdate.interval", 86400);
|
|
||||||
pref("extensions.update.enabled", true);
|
|
||||||
pref("extensions.update.interval", 86400);
|
|
||||||
pref("extensions.dss.enabled", false);
|
|
||||||
pref("extensions.dss.switchPending", false);
|
|
||||||
pref("extensions.ignoreMTimeChanges", false);
|
|
||||||
pref("extensions.logging.enabled", false);
|
|
||||||
pref("extensions.hideInstallButton", true);
|
|
||||||
pref("extensions.showMismatchUI", false);
|
|
||||||
pref("extensions.hideUpdateButton", false);
|
|
||||||
pref("extensions.strictCompatibility", false);
|
|
||||||
pref("extensions.minCompatibleAppVersion", "11.0");
|
|
||||||
|
|
||||||
pref("extensions.update.url", "https://versioncheck.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=52.9&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%¤tAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
|
|
||||||
pref("extensions.update.background.url", "https://versioncheck-bg.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=52.9&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%¤tAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
|
|
||||||
|
|
||||||
pref("extensions.hotfix.id", "firefox-android-hotfix@mozilla.org");
|
|
||||||
pref("extensions.hotfix.cert.checkAttributes", true);
|
|
||||||
pref("extensions.hotfix.certs.1.sha1Fingerprint", "91:53:98:0C:C1:86:DF:47:8F:35:22:9E:11:C9:A7:31:04:49:A1:AA");
|
|
||||||
|
|
||||||
/* preferences for the Get Add-ons pane */
|
|
||||||
pref("extensions.getAddons.cache.enabled", true);
|
|
||||||
pref("extensions.getAddons.maxResults", 15);
|
|
||||||
pref("extensions.getAddons.recommended.browseURL", "https://addons.mozilla.org/%LOCALE%/android/recommended/");
|
|
||||||
pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/list/featured/all/%MAX_RESULTS%/%OS%/%VERSION%");
|
|
||||||
pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/android/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
|
|
||||||
pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%");
|
|
||||||
pref("extensions.getAddons.browseAddons", "https://addons.mozilla.org/%LOCALE%/android/");
|
|
||||||
pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/guid:%IDS%?src=mobile&appOS=%OS%&appVersion=%VERSION%");
|
|
||||||
pref("extensions.getAddons.getWithPerformance.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/guid:%IDS%?src=mobile&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
|
|
||||||
|
|
||||||
/* preference for the locale picker */
|
|
||||||
pref("extensions.getLocales.get.url", "");
|
|
||||||
pref("extensions.compatability.locales.buildid", "0");
|
|
||||||
|
|
||||||
/* Don't let XPIProvider install distribution add-ons; we do our own thing on mobile. */
|
|
||||||
pref("extensions.installDistroAddons", false);
|
|
||||||
|
|
||||||
// Add-on content security policies.
|
|
||||||
pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
|
|
||||||
pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
|
|
||||||
|
|
||||||
/* block popups by default, and notify the user about blocked popups */
|
|
||||||
pref("dom.disable_open_during_load", true);
|
|
||||||
pref("privacy.popups.showBrowserMessage", true);
|
|
||||||
|
|
||||||
/* disable opening windows with the dialog feature */
|
|
||||||
pref("dom.disable_window_open_dialog_feature", true);
|
|
||||||
pref("dom.disable_window_showModalDialog", true);
|
|
||||||
pref("dom.disable_window_print", true);
|
|
||||||
pref("dom.disable_window_find", true);
|
|
||||||
|
|
||||||
pref("keyword.enabled", true);
|
|
||||||
pref("browser.fixup.domainwhitelist.localhost", true);
|
|
||||||
|
|
||||||
pref("accessibility.typeaheadfind", false);
|
|
||||||
pref("accessibility.typeaheadfind.timeout", 5000);
|
|
||||||
pref("accessibility.typeaheadfind.flashBar", 1);
|
|
||||||
pref("accessibility.typeaheadfind.linksonly", false);
|
|
||||||
pref("accessibility.typeaheadfind.casesensitive", 0);
|
|
||||||
pref("accessibility.browsewithcaret_shortcut.enabled", false);
|
|
||||||
|
|
||||||
// Whether the character encoding menu is under the main Firefox button. This
|
|
||||||
// preference is a string so that localizers can alter it.
|
|
||||||
pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
|
|
||||||
|
|
||||||
// pointer to the default engine name
|
|
||||||
pref("browser.search.defaultenginename", "chrome://browser/locale/region.properties");
|
|
||||||
// SSL error page behaviour
|
|
||||||
pref("browser.ssl_override_behavior", 2);
|
|
||||||
pref("browser.xul.error_pages.expert_bad_cert", false);
|
|
||||||
|
|
||||||
// ordering of search engines in the engine list.
|
|
||||||
pref("browser.search.order.1", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.search.order.2", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.search.order.3", "chrome://browser/locale/region.properties");
|
|
||||||
|
|
||||||
// Market-specific search defaults
|
|
||||||
pref("browser.search.geoSpecificDefaults", true);
|
|
||||||
pref("browser.search.geoSpecificDefaults.url", "https://search.services.mozilla.com/1/%APP%/%VERSION%/%CHANNEL%/%LOCALE%/%REGION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%");
|
|
||||||
|
|
||||||
// US specific default (used as a fallback if the geoSpecificDefaults request fails).
|
|
||||||
pref("browser.search.defaultenginename.US", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.search.order.US.1", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.search.order.US.2", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.search.order.US.3", "chrome://browser/locale/region.properties");
|
|
||||||
|
|
||||||
// disable updating
|
|
||||||
pref("browser.search.update", false);
|
|
||||||
|
|
||||||
// disable search suggestions by default
|
|
||||||
pref("browser.search.suggest.enabled", false);
|
|
||||||
pref("browser.search.suggest.prompted", false);
|
|
||||||
|
|
||||||
// tell the search service that we don't really expose the "current engine"
|
|
||||||
pref("browser.search.noCurrentEngine", true);
|
|
||||||
|
|
||||||
// Control media casting & mirroring features
|
|
||||||
pref("browser.casting.enabled", true);
|
|
||||||
#ifdef RELEASE_OR_BETA
|
|
||||||
// Chromecast mirroring is broken (bug 1131084)
|
|
||||||
pref("browser.mirroring.enabled", false);
|
|
||||||
#else
|
|
||||||
pref("browser.mirroring.enabled", true);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Enable sparse localization by setting a few package locale overrides
|
|
||||||
pref("chrome.override_package.global", "browser");
|
|
||||||
pref("chrome.override_package.mozapps", "browser");
|
|
||||||
pref("chrome.override_package.passwordmgr", "browser");
|
|
||||||
|
|
||||||
// enable xul error pages
|
|
||||||
pref("browser.xul.error_pages.enabled", true);
|
|
||||||
|
|
||||||
// disable color management
|
|
||||||
pref("gfx.color_management.mode", 0);
|
|
||||||
|
|
||||||
// 0=fixed margin, 1=velocity bias, 2=dynamic resolution, 3=no margins, 4=prediction bias
|
|
||||||
pref("gfx.displayport.strategy", 1);
|
|
||||||
|
|
||||||
// all of the following displayport strategy prefs will be divided by 1000
|
|
||||||
// to obtain some multiplier which is then used in the strategy.
|
|
||||||
// fixed margin strategy options
|
|
||||||
pref("gfx.displayport.strategy_fm.multiplier", -1); // displayport dimension multiplier
|
|
||||||
pref("gfx.displayport.strategy_fm.danger_x", -1); // danger zone on x-axis when multiplied by viewport width
|
|
||||||
pref("gfx.displayport.strategy_fm.danger_y", -1); // danger zone on y-axis when multiplied by viewport height
|
|
||||||
|
|
||||||
// velocity bias strategy options
|
|
||||||
pref("gfx.displayport.strategy_vb.multiplier", -1); // displayport dimension multiplier
|
|
||||||
pref("gfx.displayport.strategy_vb.threshold", -1); // velocity threshold in inches/frame
|
|
||||||
pref("gfx.displayport.strategy_vb.reverse_buffer", -1); // fraction of buffer to keep in reverse direction from scroll
|
|
||||||
pref("gfx.displayport.strategy_vb.danger_x_base", -1); // danger zone on x-axis when multiplied by viewport width
|
|
||||||
pref("gfx.displayport.strategy_vb.danger_y_base", -1); // danger zone on y-axis when multiplied by viewport height
|
|
||||||
pref("gfx.displayport.strategy_vb.danger_x_incr", -1); // additional danger zone on x-axis when multiplied by viewport width and velocity
|
|
||||||
pref("gfx.displayport.strategy_vb.danger_y_incr", -1); // additional danger zone on y-axis when multiplied by viewport height and velocity
|
|
||||||
|
|
||||||
// prediction bias strategy options
|
|
||||||
pref("gfx.displayport.strategy_pb.threshold", -1); // velocity threshold in inches/frame
|
|
||||||
|
|
||||||
// Allow 24-bit colour when the hardware supports it
|
|
||||||
pref("gfx.android.rgb16.force", false);
|
|
||||||
|
|
||||||
// Allow GLContexts to be attached/detached from SurfaceTextures
|
|
||||||
pref("gfx.SurfaceTexture.detach.enabled", true);
|
|
||||||
|
|
||||||
// don't allow JS to move and resize existing windows
|
|
||||||
pref("dom.disable_window_move_resize", true);
|
|
||||||
|
|
||||||
// prevent click image resizing for nsImageDocument
|
|
||||||
pref("browser.enable_click_image_resizing", false);
|
|
||||||
|
|
||||||
// open in tab preferences
|
|
||||||
// 0=default window, 1=current window/tab, 2=new window, 3=new tab in most window
|
|
||||||
pref("browser.link.open_external", 3);
|
|
||||||
pref("browser.link.open_newwindow", 3);
|
|
||||||
// 0=force all new windows to tabs, 1=don't force, 2=only force those with no features set
|
|
||||||
pref("browser.link.open_newwindow.restriction", 0);
|
|
||||||
|
|
||||||
// show images option
|
|
||||||
// 0=never, 1=always, 2=cellular-only
|
|
||||||
pref("browser.image_blocking", 1);
|
|
||||||
|
|
||||||
// controls which bits of private data to clear. by default we clear them all.
|
|
||||||
pref("privacy.item.cache", true);
|
|
||||||
pref("privacy.item.cookies", true);
|
|
||||||
pref("privacy.item.offlineApps", true);
|
|
||||||
pref("privacy.item.history", true);
|
|
||||||
pref("privacy.item.searchHistory", true);
|
|
||||||
pref("privacy.item.formdata", true);
|
|
||||||
pref("privacy.item.downloads", true);
|
|
||||||
pref("privacy.item.passwords", true);
|
|
||||||
pref("privacy.item.sessions", true);
|
|
||||||
pref("privacy.item.geolocation", true);
|
|
||||||
pref("privacy.item.siteSettings", true);
|
|
||||||
pref("privacy.item.syncAccount", true);
|
|
||||||
|
|
||||||
// enable geo
|
|
||||||
pref("geo.enabled", true);
|
|
||||||
|
|
||||||
// content sink control -- controls responsiveness during page load
|
|
||||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=481566#c9
|
|
||||||
//pref("content.sink.enable_perf_mode", 2); // 0 - switch, 1 - interactive, 2 - perf
|
|
||||||
//pref("content.sink.pending_event_mode", 0);
|
|
||||||
//pref("content.sink.perf_deflect_count", 1000000);
|
|
||||||
//pref("content.sink.perf_parse_time", 50000000);
|
|
||||||
|
|
||||||
// Disable the JS engine's gc on memory pressure, since we do one in the mobile
|
|
||||||
// browser (bug 669346).
|
|
||||||
pref("javascript.options.gc_on_memory_pressure", false);
|
|
||||||
|
|
||||||
#ifdef MOZ_PKG_SPECIAL
|
|
||||||
// low memory devices
|
|
||||||
pref("javascript.options.mem.gc_high_frequency_heap_growth_max", 120);
|
|
||||||
pref("javascript.options.mem.gc_high_frequency_heap_growth_min", 120);
|
|
||||||
pref("javascript.options.mem.gc_high_frequency_high_limit_mb", 40);
|
|
||||||
pref("javascript.options.mem.gc_high_frequency_low_limit_mb", 10);
|
|
||||||
pref("javascript.options.mem.gc_low_frequency_heap_growth", 120);
|
|
||||||
pref("javascript.options.mem.high_water_mark", 16);
|
|
||||||
pref("javascript.options.mem.gc_allocation_threshold_mb", 3);
|
|
||||||
pref("javascript.options.mem.gc_min_empty_chunk_count", 1);
|
|
||||||
pref("javascript.options.mem.gc_max_empty_chunk_count", 2);
|
|
||||||
#else
|
|
||||||
pref("javascript.options.mem.high_water_mark", 32);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
pref("dom.max_chrome_script_run_time", 0); // disable slow script dialog for chrome
|
|
||||||
pref("dom.max_script_run_time", 20);
|
|
||||||
|
|
||||||
// Absolute path to the devtools unix domain socket file used
|
|
||||||
// to communicate with a usb cable via adb forward.
|
|
||||||
pref("devtools.debugger.unix-domain-socket", "/data/data/@ANDROID_PACKAGE_NAME@/firefox-debugger-socket");
|
|
||||||
|
|
||||||
pref("devtools.remote.usb.enabled", false);
|
|
||||||
pref("devtools.remote.wifi.enabled", false);
|
|
||||||
|
|
||||||
pref("font.size.inflation.minTwips", 0);
|
|
||||||
|
|
||||||
// When true, zooming will be enabled on all sites, even ones that declare user-scalable=no.
|
|
||||||
pref("browser.ui.zoom.force-user-scalable", false);
|
|
||||||
|
|
||||||
// When removing this Nightly flag, also remember to remove the flags surrounding this feature
|
|
||||||
// in GeckoPreferences and BrowserApp (see bug 1245930).
|
|
||||||
#ifdef NIGHTLY_BUILD
|
|
||||||
pref("ui.zoomedview.enabled", true);
|
|
||||||
#else
|
|
||||||
pref("ui.zoomedview.enabled", false);
|
|
||||||
#endif
|
|
||||||
pref("ui.zoomedview.keepLimitSize", 16); // value in layer pixels, used to not keep the large elements in the cluster list (Bug 1191041)
|
|
||||||
pref("ui.zoomedview.limitReadableSize", 8); // value in layer pixels
|
|
||||||
pref("ui.zoomedview.defaultZoomFactor", 2);
|
|
||||||
pref("ui.zoomedview.simplified", true); // Do not display all the zoomed view controls, do not use size heurisistic
|
|
||||||
|
|
||||||
pref("ui.touch.radius.enabled", false);
|
|
||||||
pref("ui.touch.radius.leftmm", 3);
|
|
||||||
pref("ui.touch.radius.topmm", 5);
|
|
||||||
pref("ui.touch.radius.rightmm", 3);
|
|
||||||
pref("ui.touch.radius.bottommm", 2);
|
|
||||||
pref("ui.touch.radius.visitedWeight", 120);
|
|
||||||
|
|
||||||
pref("ui.mouse.radius.enabled", false);
|
|
||||||
pref("ui.mouse.radius.leftmm", 3);
|
|
||||||
pref("ui.mouse.radius.topmm", 5);
|
|
||||||
pref("ui.mouse.radius.rightmm", 3);
|
|
||||||
pref("ui.mouse.radius.bottommm", 2);
|
|
||||||
pref("ui.mouse.radius.visitedWeight", 120);
|
|
||||||
pref("ui.mouse.radius.reposition", true);
|
|
||||||
|
|
||||||
// The percentage of the screen that needs to be scrolled before toolbar
|
|
||||||
// manipulation is allowed.
|
|
||||||
pref("browser.ui.scroll-toolbar-threshold", 10);
|
|
||||||
|
|
||||||
// Maximum distance from the point where the user pressed where we still
|
|
||||||
// look for text to select
|
|
||||||
pref("browser.ui.selection.distance", 250);
|
|
||||||
|
|
||||||
// plugins
|
|
||||||
pref("plugin.disable", false);
|
|
||||||
pref("dom.ipc.plugins.enabled", false);
|
|
||||||
|
|
||||||
// This pref isn't actually used anymore, but we're leaving this here to avoid changing
|
|
||||||
// the default so that we can migrate a user-set pref. See bug 885357.
|
|
||||||
pref("plugins.click_to_play", true);
|
|
||||||
// The default value for nsIPluginTag.enabledState (STATE_CLICKTOPLAY = 1)
|
|
||||||
pref("plugin.default.state", 1);
|
|
||||||
|
|
||||||
// product URLs
|
|
||||||
// The breakpad report server to link to in about:crashes
|
|
||||||
pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
|
|
||||||
|
|
||||||
pref("app.support.baseURL", "https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/");
|
|
||||||
pref("app.supportURL", "https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/mobile-help");
|
|
||||||
pref("app.faqURL", "https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/faq");
|
|
||||||
|
|
||||||
// URL for feedback page
|
|
||||||
// This should be kept in sync with the "feedback_link" string defined in strings.xml.in
|
|
||||||
pref("app.feedbackURL", "https://input.mozilla.org/feedback/android/%VERSION%/%CHANNEL%/?utm_source=feedback-prompt");
|
|
||||||
|
|
||||||
pref("app.privacyURL", "https://www.mozilla.org/privacy/firefox/");
|
|
||||||
pref("app.creditsURL", "https://www.mozilla.org/credits/");
|
|
||||||
pref("app.channelURL", "https://www.mozilla.org/%LOCALE%/firefox/channel/");
|
|
||||||
#if MOZ_UPDATE_CHANNEL == aurora
|
|
||||||
pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%/auroranotes/");
|
|
||||||
#elif MOZ_UPDATE_CHANNEL == beta
|
|
||||||
pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%beta/releasenotes/");
|
|
||||||
#else
|
|
||||||
pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%/releasenotes/");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
|
|
||||||
pref("security.alternate_certificate_error_page", "certerror");
|
|
||||||
|
|
||||||
pref("security.warn_viewing_mixed", false); // Warning is disabled. See Bug 616712.
|
|
||||||
|
|
||||||
// Block insecure active content on https pages
|
|
||||||
pref("security.mixed_content.block_active_content", true);
|
|
||||||
|
|
||||||
// Enable pinning
|
|
||||||
pref("security.cert_pinning.enforcement_level", 1);
|
|
||||||
|
|
||||||
// Only fetch OCSP for EV certificates
|
|
||||||
pref("security.OCSP.enabled", 2);
|
|
||||||
|
|
||||||
// Override some named colors to avoid inverse OS themes
|
|
||||||
pref("ui.-moz-dialog", "#efebe7");
|
|
||||||
pref("ui.-moz-dialogtext", "#101010");
|
|
||||||
pref("ui.-moz-field", "#fff");
|
|
||||||
pref("ui.-moz-fieldtext", "#1a1a1a");
|
|
||||||
pref("ui.-moz-buttonhoverface", "#f3f0ed");
|
|
||||||
pref("ui.-moz-buttonhovertext", "#101010");
|
|
||||||
pref("ui.-moz-combobox", "#fff");
|
|
||||||
pref("ui.-moz-comboboxtext", "#101010");
|
|
||||||
pref("ui.buttonface", "#ece7e2");
|
|
||||||
pref("ui.buttonhighlight", "#fff");
|
|
||||||
pref("ui.buttonshadow", "#aea194");
|
|
||||||
pref("ui.buttontext", "#101010");
|
|
||||||
pref("ui.captiontext", "#101010");
|
|
||||||
pref("ui.graytext", "#b1a598");
|
|
||||||
pref("ui.highlight", "#fad184");
|
|
||||||
pref("ui.highlighttext", "#1a1a1a");
|
|
||||||
pref("ui.infobackground", "#f5f5b5");
|
|
||||||
pref("ui.infotext", "#000");
|
|
||||||
pref("ui.menu", "#f7f5f3");
|
|
||||||
pref("ui.menutext", "#101010");
|
|
||||||
pref("ui.threeddarkshadow", "#000");
|
|
||||||
pref("ui.threedface", "#ece7e2");
|
|
||||||
pref("ui.threedhighlight", "#fff");
|
|
||||||
pref("ui.threedlightshadow", "#ece7e2");
|
|
||||||
pref("ui.threedshadow", "#aea194");
|
|
||||||
pref("ui.window", "#efebe7");
|
|
||||||
pref("ui.windowtext", "#101010");
|
|
||||||
pref("ui.windowframe", "#efebe7");
|
|
||||||
|
|
||||||
/* prefs used by the update timer system (including blocklist pings) */
|
|
||||||
pref("app.update.timerFirstInterval", 30000); // milliseconds
|
|
||||||
pref("app.update.timerMinimumDelay", 30); // seconds
|
|
||||||
|
|
||||||
// used by update service to decide whether or not to
|
|
||||||
// automatically download an update
|
|
||||||
pref("app.update.autodownload", "wifi");
|
|
||||||
pref("app.update.url.android", "https://aus5.mozilla.org/update/4/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%MOZ_VERSION%/update.xml");
|
|
||||||
|
|
||||||
#ifdef MOZ_UPDATER
|
|
||||||
/* prefs used specifically for updating the app */
|
|
||||||
pref("app.update.enabled", false);
|
|
||||||
pref("app.update.channel", "@MOZ_UPDATE_CHANNEL@");
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// replace newlines with spaces on paste into single-line text boxes
|
|
||||||
pref("editor.singleLine.pasteNewlines", 2);
|
|
||||||
|
|
||||||
// threshold where a tap becomes a drag, in 1/240" reference pixels
|
|
||||||
// The names of the preferences are to be in sync with EventStateManager.cpp
|
|
||||||
pref("ui.dragThresholdX", 25);
|
|
||||||
pref("ui.dragThresholdY", 25);
|
|
||||||
|
|
||||||
pref("layers.acceleration.disabled", false);
|
|
||||||
pref("layers.async-video.enabled", true);
|
|
||||||
|
|
||||||
pref("apz.content_response_timeout", 600);
|
|
||||||
pref("apz.allow_immediate_handoff", false);
|
|
||||||
pref("apz.touch_start_tolerance", "0.06");
|
|
||||||
pref("apz.axis_lock.breakout_angle", "0.7853982"); // PI / 4 (45 degrees)
|
|
||||||
// APZ physics settings reviewed by UX
|
|
||||||
pref("apz.axis_lock.mode", 1); // Use "strict" axis locking
|
|
||||||
pref("apz.fling_curve_function_x1", "0.59");
|
|
||||||
pref("apz.fling_curve_function_y1", "0.46");
|
|
||||||
pref("apz.fling_curve_function_x2", "0.05");
|
|
||||||
pref("apz.fling_curve_function_y2", "1.00");
|
|
||||||
pref("apz.fling_curve_threshold_inches_per_ms", "0.01");
|
|
||||||
// apz.fling_friction and apz.fling_stopped_threshold are currently ignored by Fennec.
|
|
||||||
pref("apz.fling_friction", "0.004");
|
|
||||||
pref("apz.fling_stopped_threshold", "0.0");
|
|
||||||
pref("apz.max_velocity_inches_per_ms", "0.07");
|
|
||||||
pref("apz.fling_accel_interval_ms", 750);
|
|
||||||
pref("apz.overscroll.enabled", true);
|
|
||||||
|
|
||||||
pref("layers.progressive-paint", true);
|
|
||||||
pref("layers.low-precision-buffer", true);
|
|
||||||
pref("layers.low-precision-resolution", "0.25");
|
|
||||||
pref("layers.low-precision-opacity", "1.0");
|
|
||||||
// We want to limit layers for two reasons:
|
|
||||||
// 1) We can't scroll smoothly if we have to many draw calls
|
|
||||||
// 2) Pages that have too many layers consume too much memory and crash.
|
|
||||||
// By limiting the number of layers on mobile we're making the main thread
|
|
||||||
// work harder keep scrolling smooth and memory low.
|
|
||||||
pref("layers.max-active", 20);
|
|
||||||
|
|
||||||
pref("notification.feature.enabled", true);
|
|
||||||
pref("dom.webnotifications.enabled", true);
|
|
||||||
|
|
||||||
// prevent tooltips from showing up
|
|
||||||
pref("browser.chrome.toolbar_tips", false);
|
|
||||||
|
|
||||||
// don't allow meta-refresh when backgrounded
|
|
||||||
pref("browser.meta_refresh_when_inactive.disabled", true);
|
|
||||||
|
|
||||||
// prevent video elements from preloading too much data
|
|
||||||
pref("media.preload.default", 1); // default to preload none
|
|
||||||
pref("media.preload.auto", 2); // preload metadata if preload=auto
|
|
||||||
pref("media.cache_size", 32768); // 32MB media cache
|
|
||||||
// Try to save battery by not resuming reading from a connection until we fall
|
|
||||||
// below 10s of buffered data.
|
|
||||||
pref("media.cache_resume_threshold", 10);
|
|
||||||
pref("media.cache_readahead_limit", 30);
|
|
||||||
|
|
||||||
// Number of video frames we buffer while decoding video.
|
|
||||||
// On Android this is decided by a similar value which varies for
|
|
||||||
// each OMX decoder |OMX_PARAM_PORTDEFINITIONTYPE::nBufferCountMin|. This
|
|
||||||
// number must be less than the OMX equivalent or gecko will think it is
|
|
||||||
// chronically starved of video frames. All decoders seen so far have a value
|
|
||||||
// of at least 4.
|
|
||||||
pref("media.video-queue.default-size", 3);
|
|
||||||
|
|
||||||
// Enable the MediaCodec PlatformDecoderModule by default.
|
|
||||||
pref("media.android-media-codec.enabled", true);
|
|
||||||
pref("media.android-media-codec.preferred", true);
|
|
||||||
// Run decoder in seperate process.
|
|
||||||
pref("media.android-remote-codec.enabled", false);
|
|
||||||
|
|
||||||
// Enable MSE
|
|
||||||
pref("media.mediasource.enabled", true);
|
|
||||||
|
|
||||||
pref("media.mediadrm-widevinecdm.visible", true);
|
|
||||||
|
|
||||||
#ifdef MOZ_EME
|
|
||||||
// Enable EME(Encrypted media extensions?)
|
|
||||||
pref("media.eme.enabled", false);
|
|
||||||
pref("media.eme.apiVisible", false);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// optimize images memory usage
|
|
||||||
pref("image.downscale-during-decode.enabled", true);
|
|
||||||
|
|
||||||
#ifdef MOZ_SAFE_BROWSING
|
|
||||||
pref("browser.safebrowsing.downloads.enabled", false);
|
|
||||||
|
|
||||||
pref("browser.safebrowsing.id", @MOZ_APP_UA_NAME@);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// True if this is the first time we are showing about:firstrun
|
|
||||||
pref("browser.firstrun.show.uidiscovery", true);
|
|
||||||
pref("browser.firstrun.show.localepicker", false);
|
|
||||||
|
|
||||||
// True if you always want dump() to work
|
|
||||||
//
|
|
||||||
// On Android, you also need to do the following for the output
|
|
||||||
// to show up in logcat:
|
|
||||||
//
|
|
||||||
// $ adb shell stop
|
|
||||||
// $ adb shell setprop log.redirect-stdio true
|
|
||||||
// $ adb shell start
|
|
||||||
pref("browser.dom.window.dump.enabled", true);
|
|
||||||
|
|
||||||
// controls if we want camera support
|
|
||||||
pref("device.camera.enabled", true);
|
|
||||||
pref("media.realtime_decoder.enabled", true);
|
|
||||||
|
|
||||||
pref("javascript.options.showInConsole", true);
|
|
||||||
|
|
||||||
pref("full-screen-api.enabled", true);
|
|
||||||
|
|
||||||
pref("direct-texture.force.enabled", false);
|
|
||||||
pref("direct-texture.force.disabled", false);
|
|
||||||
|
|
||||||
// This fraction in 1000ths of velocity remains after every animation frame when the velocity is low.
|
|
||||||
pref("ui.scrolling.friction_slow", -1);
|
|
||||||
// This fraction in 1000ths of velocity remains after every animation frame when the velocity is high.
|
|
||||||
pref("ui.scrolling.friction_fast", -1);
|
|
||||||
// The maximum velocity change factor between events, per ms, in 1000ths.
|
|
||||||
// Direction changes are excluded.
|
|
||||||
pref("ui.scrolling.max_event_acceleration", -1);
|
|
||||||
// The rate of deceleration when the surface has overscrolled, in 1000ths.
|
|
||||||
pref("ui.scrolling.overscroll_decel_rate", -1);
|
|
||||||
// The fraction of the surface which can be overscrolled before it must snap back, in 1000ths.
|
|
||||||
pref("ui.scrolling.overscroll_snap_limit", -1);
|
|
||||||
// The minimum amount of space that must be present for an axis to be considered scrollable,
|
|
||||||
// in 1/1000ths of pixels.
|
|
||||||
pref("ui.scrolling.min_scrollable_distance", -1);
|
|
||||||
// The axis lock mode for panning behaviour - set between standard, free and sticky
|
|
||||||
pref("ui.scrolling.axis_lock_mode", "standard");
|
|
||||||
// Negate scroll, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
|
|
||||||
pref("ui.scrolling.negate_wheel_scroll", true);
|
|
||||||
// Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
|
|
||||||
// auto-detect based on reported hardware values
|
|
||||||
pref("ui.scrolling.gamepad_dead_zone", 115);
|
|
||||||
|
|
||||||
// Prefs for fling acceleration
|
|
||||||
pref("ui.scrolling.fling_accel_interval", -1);
|
|
||||||
pref("ui.scrolling.fling_accel_base_multiplier", -1);
|
|
||||||
pref("ui.scrolling.fling_accel_supplemental_multiplier", -1);
|
|
||||||
|
|
||||||
// Prefs for fling curving
|
|
||||||
pref("ui.scrolling.fling_curve_function_x1", -1);
|
|
||||||
pref("ui.scrolling.fling_curve_function_y1", -1);
|
|
||||||
pref("ui.scrolling.fling_curve_function_x2", -1);
|
|
||||||
pref("ui.scrolling.fling_curve_function_y2", -1);
|
|
||||||
pref("ui.scrolling.fling_curve_threshold_velocity", -1);
|
|
||||||
pref("ui.scrolling.fling_curve_max_velocity", -1);
|
|
||||||
pref("ui.scrolling.fling_curve_newton_iterations", -1);
|
|
||||||
|
|
||||||
// Enable accessibility mode if platform accessibility is enabled.
|
|
||||||
pref("accessibility.accessfu.activate", 2);
|
|
||||||
pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
|
|
||||||
// Active quicknav mode, index value of list from quicknav_modes
|
|
||||||
pref("accessibility.accessfu.quicknav_index", 0);
|
|
||||||
// Setting for an utterance order (0 - description first, 1 - description last).
|
|
||||||
pref("accessibility.accessfu.utterance", 1);
|
|
||||||
// Whether to skip images with empty alt text
|
|
||||||
pref("accessibility.accessfu.skip_empty_images", true);
|
|
||||||
|
|
||||||
// Transmit UDP busy-work to the LAN when anticipating low latency
|
|
||||||
// network reads and on wifi to mitigate 802.11 Power Save Polling delays
|
|
||||||
pref("network.tickle-wifi.enabled", true);
|
|
||||||
|
|
||||||
// Mobile manages state by autodetection
|
|
||||||
pref("network.manage-offline-status", true);
|
|
||||||
|
|
||||||
// increase the timeout clamp for background tabs to 15 minutes
|
|
||||||
pref("dom.min_background_timeout_value", 900000);
|
|
||||||
|
|
||||||
// Media plugins for libstagefright playback on android
|
|
||||||
pref("media.plugins.enabled", true);
|
|
||||||
|
|
||||||
// Stagefright's OMXCodec::CreationFlags. The interesting flag values are:
|
|
||||||
// 0 = Let Stagefright choose hardware or software decoding (default)
|
|
||||||
// 8 = Force software decoding
|
|
||||||
// 16 = Force hardware decoding
|
|
||||||
pref("media.stagefright.omxcodec.flags", 0);
|
|
||||||
|
|
||||||
// Coalesce touch events to prevent them from flooding the event queue
|
|
||||||
pref("dom.event.touch.coalescing.enabled", false);
|
|
||||||
|
|
||||||
// default orientation for the app, default to undefined
|
|
||||||
// the java GeckoScreenOrientationListener needs this to be defined
|
|
||||||
pref("app.orientation.default", "");
|
|
||||||
|
|
||||||
// On memory pressure, release dirty but unused pages held by jemalloc
|
|
||||||
// back to the system.
|
|
||||||
pref("memory.free_dirty_pages", true);
|
|
||||||
|
|
||||||
pref("layout.framevisibility.numscrollportwidths", 1);
|
|
||||||
pref("layout.framevisibility.numscrollportheights", 1);
|
|
||||||
|
|
||||||
pref("layers.enable-tiles", true);
|
|
||||||
|
|
||||||
// Enable the dynamic toolbar
|
|
||||||
pref("browser.chrome.dynamictoolbar", true);
|
|
||||||
|
|
||||||
// Hide common parts of URLs like "www." or "http://"
|
|
||||||
pref("browser.urlbar.trimURLs", true);
|
|
||||||
|
|
||||||
#ifdef MOZ_PKG_SPECIAL
|
|
||||||
// Disable webgl on ARMv6 because running the reftests takes
|
|
||||||
// too long for some reason (bug 843738)
|
|
||||||
pref("webgl.disabled", true);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// initial web feed readers list
|
|
||||||
pref("browser.contentHandlers.types.0.title", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.contentHandlers.types.0.uri", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.contentHandlers.types.0.type", "application/vnd.mozilla.maybe.feed");
|
|
||||||
pref("browser.contentHandlers.types.1.title", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.contentHandlers.types.1.uri", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.contentHandlers.types.1.type", "application/vnd.mozilla.maybe.feed");
|
|
||||||
pref("browser.contentHandlers.types.2.title", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.contentHandlers.types.2.uri", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.contentHandlers.types.2.type", "application/vnd.mozilla.maybe.feed");
|
|
||||||
pref("browser.contentHandlers.types.3.title", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.contentHandlers.types.3.uri", "chrome://browser/locale/region.properties");
|
|
||||||
pref("browser.contentHandlers.types.3.type", "application/vnd.mozilla.maybe.feed");
|
|
||||||
|
|
||||||
// Shortnumber matching needed for e.g. Brazil:
|
|
||||||
// 01187654321 can be found with 87654321
|
|
||||||
pref("dom.phonenumber.substringmatching.BR", 8);
|
|
||||||
pref("dom.phonenumber.substringmatching.CO", 10);
|
|
||||||
pref("dom.phonenumber.substringmatching.VE", 7);
|
|
||||||
|
|
||||||
// Enable hardware-accelerated Skia canvas
|
|
||||||
pref("gfx.canvas.azure.backends", "skia");
|
|
||||||
pref("gfx.canvas.azure.accelerated", true);
|
|
||||||
|
|
||||||
// See ua-update.json.in for the packaged UA override list
|
|
||||||
pref("general.useragent.updates.enabled", true);
|
|
||||||
pref("general.useragent.updates.url", "https://dynamicua.cdn.mozilla.net/0/%APP_ID%");
|
|
||||||
pref("general.useragent.updates.interval", 604800); // 1 week
|
|
||||||
pref("general.useragent.updates.retry", 86400); // 1 day
|
|
||||||
|
|
||||||
// When true, phone number linkification is enabled.
|
|
||||||
pref("browser.ui.linkify.phone", false);
|
|
||||||
|
|
||||||
// Enables/disables Spatial Navigation
|
|
||||||
pref("snav.enabled", true);
|
|
||||||
|
|
||||||
// This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
|
|
||||||
// this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
|
|
||||||
// repackager of this code using an alternate snippet url, please keep your users safe
|
|
||||||
pref("browser.snippets.updateUrl", "https://snippets.cdn.mozilla.net/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
|
|
||||||
|
|
||||||
// How frequently we check for new snippets, in seconds (1 day)
|
|
||||||
pref("browser.snippets.updateInterval", 86400);
|
|
||||||
|
|
||||||
// URL used to check for user's country code. Please do not directly use this code or Snippets key.
|
|
||||||
// Contact MLS team for your own credentials. https://location.services.mozilla.com/contact
|
|
||||||
pref("browser.snippets.geoUrl", "https://location.services.mozilla.com/v1/country?key=fff72d56-b040-4205-9a11-82feda9d83a3");
|
|
||||||
|
|
||||||
// URL used to ping metrics with stats about which snippets have been shown
|
|
||||||
pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
|
|
||||||
|
|
||||||
// These prefs require a restart to take effect.
|
|
||||||
pref("browser.snippets.enabled", true);
|
|
||||||
pref("browser.snippets.syncPromo.enabled", true);
|
|
||||||
pref("browser.snippets.firstrunHomepage.enabled", true);
|
|
||||||
|
|
||||||
// The mode of home provider syncing.
|
|
||||||
// 0: Sync always
|
|
||||||
// 1: Sync only when on wifi
|
|
||||||
pref("home.sync.updateMode", 0);
|
|
||||||
|
|
||||||
// How frequently to check if we should sync home provider data.
|
|
||||||
pref("home.sync.checkIntervalSecs", 3600);
|
|
||||||
|
|
||||||
// Enable device storage API
|
|
||||||
pref("device.storage.enabled", true);
|
|
||||||
|
|
||||||
// Enable meta-viewport support for font inflation code
|
|
||||||
pref("dom.meta-viewport.enabled", true);
|
|
||||||
|
|
||||||
// Enable GMP support in the addon manager.
|
|
||||||
pref("media.gmp-provider.enabled", true);
|
|
||||||
|
|
||||||
// The default color scheme in reader mode (light, dark, auto)
|
|
||||||
// auto = color automatically adjusts according to ambient light level
|
|
||||||
// (auto only works on platforms where the 'devicelight' event is enabled)
|
|
||||||
pref("reader.color_scheme", "auto");
|
|
||||||
|
|
||||||
// Color scheme values available in reader mode UI.
|
|
||||||
pref("reader.color_scheme.values", "[\"dark\",\"auto\",\"light\"]");
|
|
||||||
|
|
||||||
// Whether to use a vertical or horizontal toolbar.
|
|
||||||
pref("reader.toolbar.vertical", false);
|
|
||||||
|
|
||||||
// Telemetry settings.
|
|
||||||
// Whether to use the unified telemetry behavior, requires a restart.
|
|
||||||
pref("toolkit.telemetry.unified", false);
|
|
||||||
|
|
||||||
// Unified AccessibleCarets (touch-caret and selection-carets).
|
|
||||||
pref("layout.accessiblecaret.enabled", true);
|
|
||||||
|
|
||||||
// AccessibleCaret CSS for the Android L style assets.
|
|
||||||
pref("layout.accessiblecaret.width", "22.0");
|
|
||||||
pref("layout.accessiblecaret.height", "22.0");
|
|
||||||
pref("layout.accessiblecaret.margin-left", "-11.5");
|
|
||||||
|
|
||||||
// Android needs to show the caret when long tapping on an empty content.
|
|
||||||
pref("layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content", true);
|
|
||||||
|
|
||||||
// Androids carets are always tilt to match the text selection guideline.
|
|
||||||
pref("layout.accessiblecaret.always_tilt", true);
|
|
||||||
|
|
||||||
// Selection change notifications generated by Javascript changes
|
|
||||||
// update active AccessibleCarets / UI interactions.
|
|
||||||
pref("layout.accessiblecaret.allow_script_change_updates", true);
|
|
||||||
|
|
||||||
// Optionally provide haptic feedback on longPress selection events.
|
|
||||||
pref("layout.accessiblecaret.hapticfeedback", true);
|
|
||||||
|
|
||||||
// Initial text selection on long-press is enhanced to provide
|
|
||||||
// a smarter phone-number selection for direct-dial ActionBar action.
|
|
||||||
pref("layout.accessiblecaret.extend_selection_for_phone_number", true);
|
|
||||||
|
|
||||||
// Disable sending console to logcat on release builds.
|
|
||||||
#ifdef RELEASE_OR_BETA
|
|
||||||
pref("consoleservice.logcat", false);
|
|
||||||
#else
|
|
||||||
pref("consoleservice.logcat", true);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef RELEASE_OR_BETA
|
|
||||||
// Enable VR on mobile, making it enable by default.
|
|
||||||
pref("dom.vr.enabled", true);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
pref("browser.tabs.showAudioPlayingIcon", true);
|
|
||||||
|
|
||||||
pref("dom.serviceWorkers.enabled", false);
|
|
||||||
pref("dom.serviceWorkers.interception.enabled", false);
|
|
||||||
pref("dom.serviceWorkers.openWindow.enabled", false);
|
|
||||||
|
|
||||||
pref("dom.push.debug", false);
|
|
||||||
// The upstream autopush endpoint must have the Google API key corresponding to
|
|
||||||
// the App's sender ID; we bake this assumption directly into the URL.
|
|
||||||
pref("dom.push.serverURL", "https://updates.push.services.mozilla.com/v1/gcm/@MOZ_ANDROID_GCM_SENDERID@");
|
|
||||||
pref("dom.push.maxRecentMessageIDsPerSubscription", 0);
|
|
||||||
|
|
||||||
#ifdef MOZ_ANDROID_GCM
|
|
||||||
pref("dom.push.enabled", false);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Enable Presentation API
|
|
||||||
pref("dom.presentation.enabled", false);
|
|
||||||
pref("dom.presentation.discovery.enabled", true);
|
|
||||||
pref("dom.presentation.discovery.legacy.enabled", true); // for TV 2.5 backward capability
|
|
||||||
|
|
||||||
pref("dom.audiochannel.audioCompeting", true);
|
|
||||||
pref("dom.audiochannel.mediaControl", true);
|
|
||||||
|
|
||||||
// Space separated list of URLS that are allowed to send objects (instead of
|
|
||||||
// only strings) through webchannels. This list is duplicated in browser/app/profile/firefox.js
|
|
||||||
pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
|
|
||||||
|
|
||||||
pref("media.openUnsupportedTypeWithExternalApp", true);
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
|
||||||
# vim: set filetype=python:
|
|
||||||
# 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/.
|
|
||||||
|
|
||||||
for var in ('APP_NAME', 'APP_VERSION'):
|
|
||||||
DEFINES[var] = CONFIG['MOZ_%s' % var]
|
|
||||||
|
|
||||||
for var in ('MOZ_UPDATER', 'MOZ_APP_UA_NAME', 'ANDROID_PACKAGE_NAME'):
|
|
||||||
DEFINES[var] = CONFIG[var]
|
|
||||||
|
|
||||||
for var in ('MOZ_ANDROID_GCM', ):
|
|
||||||
if CONFIG[var]:
|
|
||||||
DEFINES[var] = 1
|
|
||||||
|
|
||||||
for var in ('MOZ_ANDROID_GCM_SENDERID', ):
|
|
||||||
if CONFIG[var]:
|
|
||||||
DEFINES[var] = CONFIG[var]
|
|
||||||
|
|
||||||
if CONFIG['MOZ_PKG_SPECIAL']:
|
|
||||||
DEFINES['MOZ_PKG_SPECIAL'] = CONFIG['MOZ_PKG_SPECIAL']
|
|
||||||
|
|
||||||
JS_PREFERENCE_PP_FILES += [
|
|
||||||
'mobile.js',
|
|
||||||
]
|
|
||||||
|
|
||||||
FINAL_TARGET_PP_FILES += [
|
|
||||||
'ua-update.json.in',
|
|
||||||
]
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
buildDir "${topobjdir}/gradle/build/mobile/android/omnijar"
|
|
||||||
|
|
||||||
apply plugin: 'java'
|
|
||||||
|
|
||||||
// This project is a dummy project; the JAR produced is not used. The :app
|
|
||||||
// project uses the set of inputs here to check if the omnijar needs to be
|
|
||||||
// rebuilt. By listing them here as resource directories, IntelliJ labels each
|
|
||||||
// checked directly nicely. Why list the directories here? There's a mismatch
|
|
||||||
// between SourceDirectorySet and TaskInputs: the former is directory oriented,
|
|
||||||
// while the latter is more general. That means its easy to convert this list
|
|
||||||
// into inputs for :app, but not vice-versa. Sadly this implies that :app
|
|
||||||
// evaluation depends on :omnijar, but the evaluation overhead is low enough
|
|
||||||
// that we accept it.
|
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
// Depend on the Gecko resources in mobile/android.
|
|
||||||
resources {
|
|
||||||
srcDir "${topsrcdir}/mobile/android/chrome"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/components"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/locales"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/modules"
|
|
||||||
srcDir "${topsrcdir}/mobile/android/themes"
|
|
||||||
srcDir "${topsrcdir}/toolkit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'idea'
|
|
||||||
|
|
||||||
idea {
|
|
||||||
module {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="org.mozilla.roboexample.test"
|
|
||||||
android:sharedUserId="${MOZ_ANDROID_SHARED_ID}"
|
|
||||||
android:versionCode="1"
|
|
||||||
android:versionName="1.0" >
|
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="${MOZ_ANDROID_MIN_SDK_VERSION}"
|
|
||||||
android:targetSdkVersion="23"/>
|
|
||||||
<!-- TODO: re-instate maxSdkVersion. -->
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
|
|
||||||
<instrumentation
|
|
||||||
android:name="org.mozilla.gecko.FennecInstrumentationTestRunner"
|
|
||||||
android:targetPackage="${ANDROID_PACKAGE_NAME}" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:label="@string/app_name">
|
|
||||||
|
|
||||||
<uses-library android:name="android.test.runner" />
|
|
||||||
|
|
||||||
<!-- Fake handlers to ensure that we have some share intents to show in our share handler list -->
|
|
||||||
<activity android:name="org.mozilla.gecko.RobocopShare1"
|
|
||||||
android:label="Robocop fake activity">
|
|
||||||
|
|
||||||
<intent-filter android:label="Fake robocop share handler 1">
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="text/*" />
|
|
||||||
<data android:mimeType="image/*" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity android:name="org.mozilla.gecko.RobocopShare2"
|
|
||||||
android:label="Robocop fake activity 2">
|
|
||||||
|
|
||||||
<intent-filter android:label="Fake robocop share handler 2">
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="text/*" />
|
|
||||||
<data android:mimeType="image/*" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity android:name="org.mozilla.gecko.LaunchFennecWithConfigurationActivity"
|
|
||||||
android:label="Robocop Fennec">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
/* Any copyright is dedicated to the Public Domain.
|
|
||||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
||||||
|
|
||||||
package org.mozilla.gecko;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
|
|
||||||
import org.robolectric.TestLifecycleApplication;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GeckoApplication isn't test-lifecycle friendly: onCreate is called multiple times, which
|
|
||||||
* re-registers Gecko event listeners, which fails. This class is magically named so that
|
|
||||||
* Robolectric uses it instead of the application defined in the Android manifest. See
|
|
||||||
* http://robolectric.blogspot.ca/2013/04/the-test-lifecycle-in-20.html.
|
|
||||||
*/
|
|
||||||
public class TestGeckoApplication extends Application implements TestLifecycleApplication {
|
|
||||||
@Override public void beforeTest(Method method) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void prepareTest(Object test) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void afterTest(Method method) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#filter slashslash
|
|
||||||
// Everything after the first // on a line will be removed by the preproccesor.
|
|
||||||
// Send these sites a custom user-agent. Bugs should be included with an entry.
|
|
||||||
// NOTE: trailing commas are not valid JSON and will prevent the CDN from syncing.
|
|
||||||
{
|
|
||||||
"weather.yahoo.co.jp": "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
|
|
||||||
// bug 1177298, lohaco.jp
|
|
||||||
"lohaco.jp": "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
|
|
||||||
// bug 1177298, www.nhk.or.jp
|
|
||||||
"nhk.or.jp": "\\)\\s# AppleWebKit ",
|
|
||||||
// bug 1177298, uniqlo.com
|
|
||||||
"uniqlo.com": "\\)\\s#) Mobile Safari ",
|
|
||||||
// bug 1338260, directv.com
|
|
||||||
"directv.com": "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36"
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
//#filter substitution
|
|
||||||
//#include @OBJDIR@/adjust_sdk_app_token
|
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.adjust.AdjustHelperInterface;
|
|
||||||
//#ifdef MOZ_INSTALL_TRACKING
|
|
||||||
import org.mozilla.gecko.adjust.AdjustHelper;
|
|
||||||
//#else
|
|
||||||
import org.mozilla.gecko.adjust.StubAdjustHelper;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public class AdjustConstants {
|
|
||||||
public static final String MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN =
|
|
||||||
//#ifdef MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN
|
|
||||||
"@MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN@";
|
|
||||||
//#else
|
|
||||||
null;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static AdjustHelperInterface getAdjustHelper() {
|
|
||||||
//#ifdef MOZ_INSTALL_TRACKING
|
|
||||||
return new AdjustHelper();
|
|
||||||
//#else
|
|
||||||
return new StubAdjustHelper();
|
|
||||||
//#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,433 +0,0 @@
|
||||||
#filter substitution
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="@ANDROID_PACKAGE_NAME@"
|
|
||||||
android:installLocation="auto"
|
|
||||||
android:versionCode="@ANDROID_VERSION_CODE@"
|
|
||||||
android:versionName="@MOZ_APP_VERSION@"
|
|
||||||
#ifdef MOZ_ANDROID_SHARED_ID
|
|
||||||
android:sharedUserId="@MOZ_ANDROID_SHARED_ID@"
|
|
||||||
#endif
|
|
||||||
>
|
|
||||||
<uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
|
|
||||||
#ifdef MOZ_ANDROID_MAX_SDK_VERSION
|
|
||||||
android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
|
|
||||||
#endif
|
|
||||||
android:targetSdkVersion="23"/>
|
|
||||||
|
|
||||||
<!-- The bouncer APK and the main APK should define the same set of
|
|
||||||
<permission>, <uses-permission>, and <uses-feature> elements. This reduces
|
|
||||||
the likelihood of permission-related surprises when installing the main APK
|
|
||||||
on top of a pre-installed bouncer APK. Add such shared elements in the
|
|
||||||
fileincluded here, so that they can be referenced by both APKs. -->
|
|
||||||
#include FennecManifest_permissions.xml.in
|
|
||||||
|
|
||||||
<application android:label="@string/moz_app_displayname"
|
|
||||||
android:icon="@drawable/icon"
|
|
||||||
android:logo="@drawable/logo"
|
|
||||||
android:name="@MOZ_ANDROID_APPLICATION_CLASS@"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:allowBackup="false"
|
|
||||||
# The preprocessor does not yet support arbitrary parentheses, so this cannot
|
|
||||||
# be parenthesized thus to clarify that the logical AND operator has precedence:
|
|
||||||
# !defined(MOZILLA_OFFICIAL) || (defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG))
|
|
||||||
#if !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG)
|
|
||||||
android:debuggable="true">
|
|
||||||
#else
|
|
||||||
android:debuggable="false">
|
|
||||||
#endif
|
|
||||||
|
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true"/>
|
|
||||||
|
|
||||||
#ifdef MOZ_NATIVE_DEVICES
|
|
||||||
<!-- This resources comes from Google Play Services. Required for casting support. -->
|
|
||||||
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
|
|
||||||
<service android:name="org.mozilla.gecko.RemotePresentationService" android:exported="false"/>
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
<!-- This activity handles all incoming Intents and dispatches them to other activities. -->
|
|
||||||
<activity android:name="org.mozilla.gecko.LauncherActivity"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
|
||||||
android:relinquishTaskIdentity="true"
|
|
||||||
android:taskAffinity=""
|
|
||||||
android:excludeFromRecents="true" />
|
|
||||||
|
|
||||||
<!-- Fennec is shipped as the Android package named
|
|
||||||
org.mozilla.{fennec,firefox,firefox_beta}. The internal Java
|
|
||||||
package hierarchy inside the Android package used to have an
|
|
||||||
org.mozilla.{fennec,firefox,firefox_beta} subtree *and* an
|
|
||||||
org.mozilla.gecko subtree; it now only has org.mozilla.gecko. -->
|
|
||||||
<activity android:name="@MOZ_ANDROID_BROWSER_INTENT_CLASS@"
|
|
||||||
android:label="@string/moz_app_displayname"
|
|
||||||
android:taskAffinity="@ANDROID_PACKAGE_NAME@.BROWSER"
|
|
||||||
android:alwaysRetainTaskState="true"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
|
|
||||||
android:windowSoftInputMode="stateUnspecified|adjustResize"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@style/Gecko.App" />
|
|
||||||
|
|
||||||
<!-- Bug 1256615 / Bug 1268455: We published an .App alias and we need to maintain it
|
|
||||||
forever. If we don't, home screen shortcuts will disappear because the intent
|
|
||||||
filter details change. -->
|
|
||||||
<activity-alias android:name=".App"
|
|
||||||
android:label="@MOZ_APP_DISPLAYNAME@"
|
|
||||||
android:targetActivity="org.mozilla.gecko.LauncherActivity">
|
|
||||||
|
|
||||||
<!-- android:priority ranges between -1000 and 1000. We never want
|
|
||||||
another activity to usurp the MAIN action, so we ratchet our
|
|
||||||
priority up. -->
|
|
||||||
<intent-filter android:priority="999">
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
|
|
||||||
<category android:name="android.intent.category.APP_BROWSER" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:scheme="about" />
|
|
||||||
<data android:scheme="javascript" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="file" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:mimeType="text/html"/>
|
|
||||||
<data android:mimeType="text/plain"/>
|
|
||||||
<data android:mimeType="application/xhtml+xml"/>
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
|
||||||
android:resource="@drawable/icon"/>
|
|
||||||
|
|
||||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
|
||||||
android:resource="@drawable/icon" />
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.mozilla.gecko.ACTION_ALERT_CALLBACK" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.mozilla.gecko.GUEST_SESSION_INPROGRESS" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.mozilla.gecko.UPDATE"/>
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.WEB_SEARCH" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
<data android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- For XPI installs from websites and the download manager. -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="file" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:mimeType="application/x-xpinstall" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- For XPI installs from file: URLs. -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:host="" />
|
|
||||||
<data android:scheme="file" />
|
|
||||||
<data android:pathPattern=".*\\.xpi" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
#ifdef MOZ_ANDROID_BEAM
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
<data android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
<!-- For debugging -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.mozilla.gecko.DEBUG" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity-alias>
|
|
||||||
|
|
||||||
<service android:name="org.mozilla.gecko.GeckoService" />
|
|
||||||
|
|
||||||
<activity android:name="org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/OverlayActivity" />
|
|
||||||
|
|
||||||
<activity android:name="org.mozilla.gecko.promotion.SimpleHelperUI"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/OverlayActivity" />
|
|
||||||
|
|
||||||
<activity android:name="org.mozilla.gecko.promotion.HomeScreenPrompt"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/OverlayActivity" />
|
|
||||||
|
|
||||||
<!-- The main reason for the Tab Queue build flag is to not mess with the VIEW intent filter
|
|
||||||
before the rest of the plumbing is in place -->
|
|
||||||
|
|
||||||
<service android:name="org.mozilla.gecko.tabqueue.TabQueueService" />
|
|
||||||
|
|
||||||
<activity android:name="org.mozilla.gecko.tabqueue.TabQueuePrompt"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/OverlayActivity" />
|
|
||||||
|
|
||||||
<receiver android:name="org.mozilla.gecko.restrictions.RestrictionProvider">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.GET_RESTRICTION_ENTRIES" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<!-- Masquerade as the Resolver so that we can be opened from the Marketplace. -->
|
|
||||||
<activity-alias
|
|
||||||
android:name="com.android.internal.app.ResolverActivity"
|
|
||||||
android:targetActivity="@MOZ_ANDROID_BROWSER_INTENT_CLASS@"
|
|
||||||
android:exported="true" />
|
|
||||||
|
|
||||||
<receiver android:name="org.mozilla.gecko.GeckoUpdateReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="@ANDROID_PACKAGE_NAME@.CHECK_UPDATE_RESULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver android:name="org.mozilla.gecko.GeckoMessageReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<!-- Catch install referrer so we can do post-install work. -->
|
|
||||||
<receiver android:name="org.mozilla.gecko.distribution.ReferrerReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.android.vending.INSTALL_REFERRER" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service android:name="org.mozilla.gecko.Restarter"
|
|
||||||
android:exported="false"
|
|
||||||
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.Restarter">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service android:name="org.mozilla.gecko.media.MediaControlService"
|
|
||||||
android:exported="false">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<receiver android:name="org.mozilla.gecko.AlarmReceiver" >
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="org.mozilla.gecko.notifications.WhatsNewReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
|
||||||
<data android:scheme="package" android:path="org.mozilla.gecko" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="org.mozilla.gecko.notifications.NotificationReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<!-- Notification API V2 -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="@ANDROID_PACKAGE_NAME@.helperBroadcastAction" />
|
|
||||||
<action android:name="@ANDROID_PACKAGE_NAME@.NOTIFICATION_CLICK" />
|
|
||||||
<action android:name="@ANDROID_PACKAGE_NAME@.NOTIFICATION_CLOSE" />
|
|
||||||
<data android:scheme="moz-notification" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
#include ../services/manifests/FxAccountAndroidManifest_activities.xml.in
|
|
||||||
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
|
||||||
#include ../search/manifests/SearchAndroidManifest_activities.xml.in
|
|
||||||
#endif
|
|
||||||
|
|
||||||
<activity android:name="org.mozilla.gecko.preferences.GeckoPreferences"
|
|
||||||
android:theme="@style/Gecko.Preferences"
|
|
||||||
android:configChanges="orientation|screenSize|locale|layoutDirection"
|
|
||||||
android:excludeFromRecents="true"/>
|
|
||||||
|
|
||||||
<provider android:name="org.mozilla.gecko.db.BrowserProvider"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<provider android:name="org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.partnerbookmarks"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<!-- Share overlay activity
|
|
||||||
|
|
||||||
Setting launchMode="singleTop" ensures onNewIntent is called when the Activity is
|
|
||||||
reused. Ideally we create a new instance but Android L breaks this (bug 1137928). -->
|
|
||||||
<activity android:name="org.mozilla.gecko.overlays.ui.ShareDialog"
|
|
||||||
android:label="@string/overlay_share_label"
|
|
||||||
android:theme="@style/OverlayActivity"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
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>
|
|
||||||
|
|
||||||
#ifdef MOZ_ANDROID_CUSTOM_TABS
|
|
||||||
<activity android:name="org.mozilla.gecko.customtabs.CustomTabsActivity"
|
|
||||||
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
|
||||||
#endif
|
|
||||||
|
|
||||||
<!-- Service to handle requests from overlays. -->
|
|
||||||
<service android:name="org.mozilla.gecko.overlays.service.OverlayActionService" />
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Ensure that passwords provider runs in its own process. (Bug 718760.)
|
|
||||||
Process name is per-application to avoid loading CPs from multiple
|
|
||||||
Fennec versions into the same process. (Bug 749727.)
|
|
||||||
Process name is a mangled version to avoid a Talos bug. (Bug 750548.)
|
|
||||||
-->
|
|
||||||
<provider android:name="org.mozilla.gecko.db.PasswordsProvider"
|
|
||||||
android:label="@string/sync_configure_engines_title_passwords"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.passwords"
|
|
||||||
android:exported="false"
|
|
||||||
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.PasswordsProvider"/>
|
|
||||||
|
|
||||||
<provider android:name="org.mozilla.gecko.db.LoginsProvider"
|
|
||||||
android:label="@string/sync_configure_engines_title_passwords"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.logins"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<provider android:name="org.mozilla.gecko.db.FormHistoryProvider"
|
|
||||||
android:label="@string/sync_configure_engines_title_history"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.formhistory"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<provider android:name="org.mozilla.gecko.GeckoProfilesProvider"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.profiles"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<provider android:name="org.mozilla.gecko.db.TabsProvider"
|
|
||||||
android:label="@string/sync_configure_engines_title_tabs"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.tabs"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<provider android:name="org.mozilla.gecko.db.HomeProvider"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.home"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<provider android:name="org.mozilla.gecko.db.SearchHistoryProvider"
|
|
||||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.searchhistory"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:exported="false"
|
|
||||||
android:name="org.mozilla.gecko.updater.UpdateService"
|
|
||||||
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:exported="false"
|
|
||||||
android:name="org.mozilla.gecko.notifications.NotificationService">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:exported="false"
|
|
||||||
android:name="org.mozilla.gecko.dlc.DownloadContentService">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:exported="false"
|
|
||||||
android:name="org.mozilla.gecko.feeds.FeedService">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<!-- DON'T EXPORT THIS, please! An attacker could delete arbitrary files. -->
|
|
||||||
<service
|
|
||||||
android:exported="false"
|
|
||||||
android:name="org.mozilla.gecko.cleanup.FileCleanupService">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="org.mozilla.gecko.feeds.FeedAlarmReceiver"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="org.mozilla.gecko.BootReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="org.mozilla.gecko.PackageReplacedReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"></action>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="org.mozilla.gecko.telemetry.TelemetryUploadService"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
#ifdef MOZ_ANDROID_CUSTOM_TABS
|
|
||||||
<service
|
|
||||||
android:name="org.mozilla.gecko.customtabs.GeckoCustomTabsService"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include ../services/manifests/FxAccountAndroidManifest_services.xml.in
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="org.mozilla.gecko.tabqueue.TabReceivedService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
|
||||||
#include ../search/manifests/SearchAndroidManifest_services.xml.in
|
|
||||||
#endif
|
|
||||||
#ifdef MOZ_ANDROID_MLS_STUMBLER
|
|
||||||
#include ../stumbler/manifests/StumblerManifest_services.xml.in
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef MOZ_ANDROID_GCM
|
|
||||||
#include GcmAndroidManifest_services.xml.in
|
|
||||||
#endif
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="org.mozilla.gecko.media.MediaManager"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false"
|
|
||||||
android:process=":media"
|
|
||||||
android:isolatedProcess="false">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
|
|
@ -1,342 +0,0 @@
|
||||||
//#filter substitution
|
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
//#ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
|
|
||||||
import android.support.multidex.MultiDex;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A collection of constants that pertain to the build and runtime state of the
|
|
||||||
* application. Typically these are sourced from build-time definitions (see
|
|
||||||
* Makefile.in). This is a Java-side substitute for nsIXULAppInfo, amongst
|
|
||||||
* other things.
|
|
||||||
*
|
|
||||||
* See also SysInfo.java, which includes some of the values available from
|
|
||||||
* nsSystemInfo inside Gecko.
|
|
||||||
*/
|
|
||||||
// Normally, we'd annotate with @RobocopTarget. Since AppConstants is compiled
|
|
||||||
// before RobocopTarget, we instead add o.m.g.AppConstants directly to the
|
|
||||||
// Proguard configuration.
|
|
||||||
public class AppConstants {
|
|
||||||
public static final String ANDROID_PACKAGE_NAME = "@ANDROID_PACKAGE_NAME@";
|
|
||||||
public static final String MANGLED_ANDROID_PACKAGE_NAME = "@MANGLED_ANDROID_PACKAGE_NAME@";
|
|
||||||
|
|
||||||
public static final String MOZ_ANDROID_SHARED_FXACCOUNT_TYPE = "@ANDROID_PACKAGE_NAME@_fxaccount";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encapsulates access to compile-time version definitions, allowing
|
|
||||||
* for dead code removal for particular APKs.
|
|
||||||
*/
|
|
||||||
public static final class Versions {
|
|
||||||
public static final int MIN_SDK_VERSION = @MOZ_ANDROID_MIN_SDK_VERSION@;
|
|
||||||
public static final int MAX_SDK_VERSION =
|
|
||||||
//#ifdef MOZ_ANDROID_MAX_SDK_VERSION
|
|
||||||
@MOZ_ANDROID_MAX_SDK_VERSION@;
|
|
||||||
//#else
|
|
||||||
999;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The SDK_INT >= N check can only pass if our MAX_SDK_VERSION is
|
|
||||||
* _greater than or equal_ to that number, because otherwise we
|
|
||||||
* won't be installed on the device.
|
|
||||||
*
|
|
||||||
* If MIN_SDK_VERSION is greater than or equal to the number, there
|
|
||||||
* is no need to do the runtime check.
|
|
||||||
*/
|
|
||||||
public static final boolean feature16Plus = MIN_SDK_VERSION >= 16 || (MAX_SDK_VERSION >= 16 && Build.VERSION.SDK_INT >= 16);
|
|
||||||
public static final boolean feature17Plus = MIN_SDK_VERSION >= 17 || (MAX_SDK_VERSION >= 17 && Build.VERSION.SDK_INT >= 17);
|
|
||||||
public static final boolean feature19Plus = MIN_SDK_VERSION >= 19 || (MAX_SDK_VERSION >= 19 && Build.VERSION.SDK_INT >= 19);
|
|
||||||
public static final boolean feature20Plus = MIN_SDK_VERSION >= 20 || (MAX_SDK_VERSION >= 20 && Build.VERSION.SDK_INT >= 20);
|
|
||||||
public static final boolean feature21Plus = MIN_SDK_VERSION >= 21 || (MAX_SDK_VERSION >= 21 && Build.VERSION.SDK_INT >= 21);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If our MIN_SDK_VERSION is 14 or higher, we must be an ICS device.
|
|
||||||
* If our MAX_SDK_VERSION is lower than ICS, we must not be an ICS device.
|
|
||||||
* Otherwise, we need a range check.
|
|
||||||
*/
|
|
||||||
public static final boolean preMarshmallow = MAX_SDK_VERSION < 23 || (MIN_SDK_VERSION < 23 && Build.VERSION.SDK_INT < 23);
|
|
||||||
public static final boolean preLollipop = MAX_SDK_VERSION < 21 || (MIN_SDK_VERSION < 21 && Build.VERSION.SDK_INT < 21);
|
|
||||||
public static final boolean preJBMR2 = MAX_SDK_VERSION < 18 || (MIN_SDK_VERSION < 18 && Build.VERSION.SDK_INT < 18);
|
|
||||||
public static final boolean preJBMR1 = MAX_SDK_VERSION < 17 || (MIN_SDK_VERSION < 17 && Build.VERSION.SDK_INT < 17);
|
|
||||||
public static final boolean preJB = MAX_SDK_VERSION < 16 || (MIN_SDK_VERSION < 16 && Build.VERSION.SDK_INT < 16);
|
|
||||||
public static final boolean preN = MAX_SDK_VERSION < 24 || (MIN_SDK_VERSION < 24 && Build.VERSION.SDK_INT < 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the Java class that represents the android application.
|
|
||||||
*/
|
|
||||||
public static final String MOZ_ANDROID_APPLICATION_CLASS = "@MOZ_ANDROID_APPLICATION_CLASS@";
|
|
||||||
/**
|
|
||||||
* The name of the Java class that launches the browser activity.
|
|
||||||
*/
|
|
||||||
public static final String MOZ_ANDROID_BROWSER_INTENT_CLASS = "@MOZ_ANDROID_BROWSER_INTENT_CLASS@";
|
|
||||||
/**
|
|
||||||
* The name of the Java class that launches the search activity.
|
|
||||||
*/
|
|
||||||
public static final String MOZ_ANDROID_SEARCH_INTENT_CLASS = "@MOZ_ANDROID_SEARCH_INTENT_CLASS@";
|
|
||||||
|
|
||||||
public static final String GRE_MILESTONE = "@GRE_MILESTONE@";
|
|
||||||
|
|
||||||
public static final String MOZ_APP_ABI = "@MOZ_APP_ABI@";
|
|
||||||
public static final String MOZ_APP_BASENAME = "@MOZ_APP_BASENAME@";
|
|
||||||
|
|
||||||
// For the benefit of future archaeologists:
|
|
||||||
// GRE_BUILDID is exactly the same as MOZ_APP_BUILDID unless you're running
|
|
||||||
// on XULRunner, which is never the case on Android.
|
|
||||||
public static final String MOZ_APP_BUILDID = "@MOZ_BUILDID@";
|
|
||||||
public static final String MOZ_APP_ID = "@MOZ_APP_ID@";
|
|
||||||
public static final String MOZ_APP_NAME = "@MOZ_APP_NAME@";
|
|
||||||
public static final String MOZ_APP_VENDOR = "@MOZ_APP_VENDOR@";
|
|
||||||
public static final String MOZ_APP_VERSION = "@MOZ_APP_VERSION@";
|
|
||||||
public static final String MOZ_APP_DISPLAYNAME = "@MOZ_APP_DISPLAYNAME@";
|
|
||||||
// MOZ_APP_UA_NAME is already quoted when it gets substituted, like MOZILLA_VERSION.
|
|
||||||
public static final String MOZ_APP_UA_NAME = @MOZ_APP_UA_NAME@;
|
|
||||||
|
|
||||||
// MOZILLA_VERSION is already quoted when it gets substituted in. If we
|
|
||||||
// add additional quotes we end up with ""x.y"", which is a syntax error.
|
|
||||||
public static final String MOZILLA_VERSION = @MOZILLA_VERSION@;
|
|
||||||
|
|
||||||
public static final String MOZ_MOZILLA_API_KEY = "@MOZ_MOZILLA_API_KEY@";
|
|
||||||
public static final boolean MOZ_STUMBLER_BUILD_TIME_ENABLED =
|
|
||||||
//#ifdef MOZ_ANDROID_MLS_STUMBLER
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_ANDROID_GCM =
|
|
||||||
//#ifdef MOZ_ANDROID_GCM
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final String MOZ_ANDROID_GCM_SENDERID =
|
|
||||||
//#ifdef MOZ_ANDROID_GCM_SENDERID
|
|
||||||
"@MOZ_ANDROID_GCM_SENDERID@";
|
|
||||||
//#else
|
|
||||||
null;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final String MOZ_CHILD_PROCESS_NAME = "@MOZ_CHILD_PROCESS_NAME@";
|
|
||||||
public static final String MOZ_UPDATE_CHANNEL = "@MOZ_UPDATE_CHANNEL@";
|
|
||||||
public static final String OMNIJAR_NAME = "@OMNIJAR_NAME@";
|
|
||||||
public static final String OS_TARGET = "@OS_TARGET@";
|
|
||||||
public static final String TARGET_XPCOM_ABI = @TARGET_XPCOM_ABI@;
|
|
||||||
|
|
||||||
public static final String USER_AGENT_BOT_LIKE = "Redirector/" + AppConstants.MOZ_APP_VERSION +
|
|
||||||
" (Android; rv:" + AppConstants.MOZ_APP_VERSION + ")";
|
|
||||||
|
|
||||||
public static final String USER_AGENT_FENNEC_MOBILE = "Mozilla/5.0 (Android " +
|
|
||||||
Build.VERSION.RELEASE + "; Mobile; rv:" +
|
|
||||||
AppConstants.MOZ_APP_VERSION + ") Gecko/" +
|
|
||||||
AppConstants.MOZ_APP_VERSION + " Firefox/" +
|
|
||||||
AppConstants.MOZ_APP_VERSION;
|
|
||||||
|
|
||||||
public static final String USER_AGENT_FENNEC_TABLET = "Mozilla/5.0 (Android " +
|
|
||||||
Build.VERSION.RELEASE + "; Tablet; rv:" +
|
|
||||||
AppConstants.MOZ_APP_VERSION + ") Gecko/" +
|
|
||||||
AppConstants.MOZ_APP_VERSION + " Firefox/" +
|
|
||||||
AppConstants.MOZ_APP_VERSION;
|
|
||||||
|
|
||||||
public static final int MOZ_MIN_CPU_VERSION = @MOZ_MIN_CPU_VERSION@;
|
|
||||||
|
|
||||||
public static final boolean MOZ_ANDROID_ANR_REPORTER =
|
|
||||||
//#ifdef MOZ_ANDROID_ANR_REPORTER
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final String MOZ_PKG_SPECIAL =
|
|
||||||
//#ifdef MOZ_PKG_SPECIAL
|
|
||||||
"@MOZ_PKG_SPECIAL@";
|
|
||||||
//#else
|
|
||||||
null;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_EXCLUDE_HYPHENATION_DICTIONARIES =
|
|
||||||
//#ifdef MOZ_EXCLUDE_HYPHENATION_DICTIONARIES
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_SERVICES_HEALTHREPORT =
|
|
||||||
//#ifdef MOZ_SERVICES_HEALTHREPORT
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_TELEMETRY_ON_BY_DEFAULT =
|
|
||||||
//#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final String TELEMETRY_PREF_NAME =
|
|
||||||
"toolkit.telemetry.enabled";
|
|
||||||
|
|
||||||
public static final boolean MOZ_TELEMETRY_REPORTING =
|
|
||||||
//#ifdef MOZ_TELEMETRY_REPORTING
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_DATA_REPORTING =
|
|
||||||
//#ifdef MOZ_DATA_REPORTING
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_LOCALE_SWITCHER =
|
|
||||||
//#ifdef MOZ_LOCALE_SWITCHER
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_UPDATER =
|
|
||||||
//#ifdef MOZ_UPDATER
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
// Android Beam is only supported on API14+, so we don't even bother building
|
|
||||||
// it if this APK doesn't include API14 support.
|
|
||||||
public static final boolean MOZ_ANDROID_BEAM =
|
|
||||||
//#ifdef MOZ_ANDROID_BEAM
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
// See this wiki page for more details about channel specific build defines:
|
|
||||||
// https://wiki.mozilla.org/Platform/Channel-specific_build_defines
|
|
||||||
public static final boolean RELEASE_OR_BETA =
|
|
||||||
//#ifdef RELEASE_OR_BETA
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean NIGHTLY_BUILD =
|
|
||||||
//#ifdef NIGHTLY_BUILD
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean DEBUG_BUILD =
|
|
||||||
//#ifdef MOZ_DEBUG
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_MEDIA_PLAYER =
|
|
||||||
//#ifdef MOZ_NATIVE_DEVICES
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
// Official corresponds, roughly, to whether this build is performed on
|
|
||||||
// Mozilla's continuous integration infrastructure. You should disable
|
|
||||||
// developer-only functionality when this flag is set.
|
|
||||||
public static final boolean MOZILLA_OFFICIAL =
|
|
||||||
//#ifdef MOZILLA_OFFICIAL
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean ANDROID_DOWNLOADS_INTEGRATION =
|
|
||||||
//#ifdef MOZ_ANDROID_DOWNLOADS_INTEGRATION
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_DRAGGABLE_URLBAR = false;
|
|
||||||
|
|
||||||
public static final boolean MOZ_INSTALL_TRACKING =
|
|
||||||
//#ifdef MOZ_INSTALL_TRACKING
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_SWITCHBOARD =
|
|
||||||
//#ifdef MOZ_SWITCHBOARD
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Target CPU architecture: "armeabi-v7a", "x86, "mips", ..
|
|
||||||
*/
|
|
||||||
public static final String ANDROID_CPU_ARCH = "@ANDROID_CPU_ARCH@";
|
|
||||||
|
|
||||||
public static final boolean MOZ_ANDROID_EXCLUDE_FONTS =
|
|
||||||
//#ifdef MOZ_ANDROID_EXCLUDE_FONTS
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE =
|
|
||||||
//#ifdef MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
public static final boolean MOZ_ANDROID_CUSTOM_TABS =
|
|
||||||
//#ifdef MOZ_ANDROID_CUSTOM_TABS
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
// (bug 1266820) Temporarily disabled since no one is working on it.
|
|
||||||
public static final boolean SCREENSHOTS_IN_BOOKMARKS_ENABLED = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables multidex depending on build flags. For more information,
|
|
||||||
* see `multiDexEnabled true` in mobile/android/app/build.gradle.
|
|
||||||
*
|
|
||||||
* As a method, this shouldn't be in AppConstants, but it's
|
|
||||||
* the only semi-relevant Java file that we pre-process.
|
|
||||||
*/
|
|
||||||
public static void maybeInstallMultiDex(final Context context) {
|
|
||||||
//#ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
|
|
||||||
if (BuildConfig.FLAVOR.equals("automation")) {
|
|
||||||
MultiDex.install(context);
|
|
||||||
}
|
|
||||||
//#else
|
|
||||||
// Do nothing.
|
|
||||||
//#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final boolean MOZ_ANDROID_ACTIVITY_STREAM =
|
|
||||||
//#ifdef MOZ_ANDROID_ACTIVITY_STREAM
|
|
||||||
true;
|
|
||||||
//#else
|
|
||||||
false;
|
|
||||||
//#endif
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
<!-- The bouncer APK and the Fennec APK should define the same set of
|
|
||||||
<permission>, <uses-permission>, and <uses-feature> elements. This reduces
|
|
||||||
the likelihood of permission-related surprises when installing the main APK
|
|
||||||
on top of a pre-installed bouncer APK. Add such elements here, so that
|
|
||||||
they can be easily shared between the two APKs. -->
|
|
||||||
|
|
||||||
#include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
|
|
||||||
|
|
||||||
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
|
||||||
#include ../search/manifests/SearchAndroidManifest_permissions.xml.in
|
|
||||||
#endif
|
|
||||||
|
|
||||||
<!-- Bug 1261302: we have two new permissions to request,
|
|
||||||
RECEIVE_BOOT_COMPLETED and the permission for push. We want to ask for
|
|
||||||
them during the same release, which should be Fennec 48. Therefore we
|
|
||||||
decouple the push permission from MOZ_ANDROID_GCM to let it ride ahead
|
|
||||||
(potentially) of the push feature. -->
|
|
||||||
#include GcmAndroidManifest_permissions.xml.in
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
|
||||||
<!-- READ_EXTERNAL_STORAGE was added in API 16, and is only enforced in API
|
|
||||||
19+. We declare it so that the bouncer APK and the main APK have the
|
|
||||||
same set of permissions. -->
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
|
||||||
<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
|
|
||||||
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
|
||||||
#ifdef MOZ_ANDROID_DOWNLOADS_INTEGRATION
|
|
||||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
|
||||||
#endif
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.location" android:required="false"/>
|
|
||||||
<uses-feature android:name="android.hardware.location.gps" android:required="false"/>
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen"/>
|
|
||||||
|
|
||||||
<!-- Tab Queue -->
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
|
||||||
|
|
||||||
#ifdef MOZ_ANDROID_BEAM
|
|
||||||
<!-- Android Beam support -->
|
|
||||||
<uses-permission android:name="android.permission.NFC"/>
|
|
||||||
<uses-feature android:name="android.hardware.nfc" android:required="false"/>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef MOZ_WEBRTC
|
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
|
||||||
<uses-feature android:name="android.hardware.audio.low_latency" android:required="false"/>
|
|
||||||
<uses-feature android:name="android.hardware.camera.any" android:required="false"/>
|
|
||||||
<uses-feature android:name="android.hardware.microphone" android:required="false"/>
|
|
||||||
#endif
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
|
||||||
|
|
||||||
<!-- App requires OpenGL ES 2.0 -->
|
|
||||||
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
|
||||||
<!-- Avoid a linter warning by not double-including WAKE_LOCK.
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
-->
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<!-- Handle GCM registration updates from on-device Google Play Services. -->
|
|
||||||
<service
|
|
||||||
android:name="org.mozilla.gecko.gcm.GcmInstanceIDListenerService"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.android.gms.iid.InstanceID"/>
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<!-- Provided by on-device Google Play Services. Directs inbound messages to internal listener service. -->
|
|
||||||
<receiver
|
|
||||||
android:name="com.google.android.gms.gcm.GcmReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="com.google.android.c2dm.permission.SEND">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
|
|
||||||
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
|
|
||||||
<category android:name="@ANDROID_PACKAGE_NAME@" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<!-- Handle messages directed by the GCM receiver. -->
|
|
||||||
<service
|
|
||||||
android:name="org.mozilla.gecko.gcm.GcmMessageListenerService"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
@ -1,594 +0,0 @@
|
||||||
# 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/.
|
|
||||||
|
|
||||||
# We call mach -> Make -> gradle -> mach, which races to find and
|
|
||||||
# create .mozconfig files and to generate targets.
|
|
||||||
ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
|
|
||||||
.NOTPARALLEL:
|
|
||||||
endif
|
|
||||||
|
|
||||||
MOZ_BUILDID := $(shell awk '{print $$3}' $(DEPTH)/buildid.h)
|
|
||||||
|
|
||||||
# Set the appropriate version code, based on the existance of the
|
|
||||||
# MOZ_APP_ANDROID_VERSION_CODE variable.
|
|
||||||
ifdef MOZ_APP_ANDROID_VERSION_CODE
|
|
||||||
ANDROID_VERSION_CODE:=$(MOZ_APP_ANDROID_VERSION_CODE)
|
|
||||||
else
|
|
||||||
ANDROID_VERSION_CODE:=$(shell $(PYTHON) \
|
|
||||||
$(topsrcdir)/python/mozbuild/mozbuild/android_version_code.py \
|
|
||||||
--verbose \
|
|
||||||
--with-android-cpu-arch=$(ANDROID_CPU_ARCH) \
|
|
||||||
$(if $(MOZ_ANDROID_MIN_SDK_VERSION),--with-android-min-sdk=$(MOZ_ANDROID_MIN_SDK_VERSION)) \
|
|
||||||
$(if $(MOZ_ANDROID_MAX_SDK_VERSION),--with-android-max-sdk=$(MOZ_ANDROID_MAX_SDK_VERSION)) \
|
|
||||||
$(MOZ_BUILDID))
|
|
||||||
endif
|
|
||||||
|
|
||||||
DEFINES += \
|
|
||||||
-DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \
|
|
||||||
-DMOZ_ANDROID_SHARED_ID="$(MOZ_ANDROID_SHARED_ID)" \
|
|
||||||
-DMOZ_BUILDID=$(MOZ_BUILDID) \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
GARBAGE += \
|
|
||||||
classes.dex \
|
|
||||||
gecko.ap_ \
|
|
||||||
res/values/strings.xml \
|
|
||||||
res/raw/browsersearch.json \
|
|
||||||
res/raw/suggestedsites.json \
|
|
||||||
.aapt.deps \
|
|
||||||
GeneratedJNINatives.h \
|
|
||||||
GeneratedJNIWrappers.cpp \
|
|
||||||
GeneratedJNIWrappers.h \
|
|
||||||
FennecJNINatives.h \
|
|
||||||
FennecJNIWrappers.cpp \
|
|
||||||
FennecJNIWrappers.h \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
GARBAGE_DIRS += classes db jars res sync services generated
|
|
||||||
|
|
||||||
# The bootclasspath is functionally identical to the classpath, but allows the
|
|
||||||
# classes given to redefine classes in core packages, such as java.lang.
|
|
||||||
# android.jar is here as it provides Android's definition of the Java Standard
|
|
||||||
# Library. The compatability lib here tweaks a few of the core classes to paint
|
|
||||||
# over changes in behaviour between versions.
|
|
||||||
JAVA_BOOTCLASSPATH := \
|
|
||||||
$(ANDROID_SDK)/android.jar \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
JAVA_BOOTCLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_BOOTCLASSPATH)))
|
|
||||||
|
|
||||||
JAVA_CLASSPATH += \
|
|
||||||
$(ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB) \
|
|
||||||
$(ANDROID_SUPPORT_V4_AAR_LIB) \
|
|
||||||
$(ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB) \
|
|
||||||
$(ANDROID_APPCOMPAT_V7_AAR_LIB) \
|
|
||||||
$(ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR_LIB) \
|
|
||||||
$(ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR_LIB) \
|
|
||||||
$(ANDROID_CARDVIEW_V7_AAR_LIB) \
|
|
||||||
$(ANDROID_DESIGN_AAR_LIB) \
|
|
||||||
$(ANDROID_RECYCLERVIEW_V7_AAR_LIB) \
|
|
||||||
$(ANDROID_CUSTOMTABS_AAR_LIB) \
|
|
||||||
$(ANDROID_PALETTE_V7_AAR_LIB) \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
# If native devices are enabled, add Google Play Services and some of the v7
|
|
||||||
# compat libraries.
|
|
||||||
ifdef MOZ_NATIVE_DEVICES
|
|
||||||
JAVA_CLASSPATH += \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_CAST_AAR_LIB) \
|
|
||||||
$(ANDROID_MEDIAROUTER_V7_AAR_LIB) \
|
|
||||||
$(ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB) \
|
|
||||||
$(NULL)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifdef MOZ_ANDROID_GCM
|
|
||||||
JAVA_CLASSPATH += \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB) \
|
|
||||||
$(NULL)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifdef MOZ_INSTALL_TRACKING
|
|
||||||
JAVA_CLASSPATH += \
|
|
||||||
$(ANDROID_PLAY_SERVICES_ADS_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
|
|
||||||
$(NULL)
|
|
||||||
endif
|
|
||||||
|
|
||||||
JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
|
|
||||||
|
|
||||||
# Library jars that we're bundling: these are subject to Proguard before inclusion
|
|
||||||
# into classes.dex.
|
|
||||||
java_bundled_libs := \
|
|
||||||
$(ANDROID_SUPPORT_V4_AAR_LIB) \
|
|
||||||
$(ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB) \
|
|
||||||
$(ANDROID_APPCOMPAT_V7_AAR_LIB) \
|
|
||||||
$(ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR_LIB) \
|
|
||||||
$(ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR_LIB) \
|
|
||||||
$(ANDROID_CARDVIEW_V7_AAR_LIB) \
|
|
||||||
$(ANDROID_DESIGN_AAR_LIB) \
|
|
||||||
$(ANDROID_RECYCLERVIEW_V7_AAR_LIB) \
|
|
||||||
$(ANDROID_CUSTOMTABS_AAR_LIB) \
|
|
||||||
$(ANDROID_PALETTE_V7_AAR_LIB) \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
ifdef MOZ_NATIVE_DEVICES
|
|
||||||
java_bundled_libs += \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_CAST_AAR_LIB) \
|
|
||||||
$(ANDROID_MEDIAROUTER_V7_AAR_LIB) \
|
|
||||||
$(ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB) \
|
|
||||||
$(NULL)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifdef MOZ_ANDROID_GCM
|
|
||||||
java_bundled_libs += \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB) \
|
|
||||||
$(NULL)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifdef MOZ_INSTALL_TRACKING
|
|
||||||
java_bundled_libs += \
|
|
||||||
$(ANDROID_PLAY_SERVICES_ADS_AAR_LIB) \
|
|
||||||
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
|
|
||||||
$(NULL)
|
|
||||||
endif
|
|
||||||
|
|
||||||
# uniq purloined from http://stackoverflow.com/a/16151140.
|
|
||||||
uniq = $(if $1,$(firstword $1) $(call uniq,$(filter-out $(firstword $1),$1)))
|
|
||||||
|
|
||||||
java_bundled_libs := $(call uniq,$(java_bundled_libs))
|
|
||||||
java_bundled_libs := $(subst $(NULL) ,:,$(strip $(java_bundled_libs)))
|
|
||||||
|
|
||||||
GECKOVIEW_JARS = \
|
|
||||||
constants.jar \
|
|
||||||
gecko-R.jar \
|
|
||||||
gecko-mozglue.jar \
|
|
||||||
gecko-util.jar \
|
|
||||||
gecko-view.jar \
|
|
||||||
sync-thirdparty.jar \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
ifdef MOZ_INSTALL_TRACKING
|
|
||||||
GECKOVIEW_JARS += gecko-thirdparty-adjust_sdk.jar
|
|
||||||
endif
|
|
||||||
|
|
||||||
geckoview_jars_classpath := $(subst $(NULL) ,:,$(strip $(GECKOVIEW_JARS)))
|
|
||||||
|
|
||||||
FENNEC_JARS = \
|
|
||||||
gecko-browser.jar \
|
|
||||||
gecko-thirdparty.jar \
|
|
||||||
services.jar \
|
|
||||||
../javaaddons/javaaddons-1.0.jar \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
ifdef MOZ_WEBRTC
|
|
||||||
FENNEC_JARS += webrtc.jar
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
|
||||||
FENNEC_JARS += search-activity.jar
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifdef MOZ_ANDROID_MLS_STUMBLER
|
|
||||||
FENNEC_JARS += ../stumbler/stumbler.jar
|
|
||||||
endif
|
|
||||||
|
|
||||||
# All the jars we're compiling from source. (not to be confused with
|
|
||||||
# java_bundled_libs, which holds the jars which we're including as binaries).
|
|
||||||
ALL_JARS = \
|
|
||||||
$(GECKOVIEW_JARS) \
|
|
||||||
$(FENNEC_JARS) \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
# The list of jars in Java classpath notation (colon-separated).
|
|
||||||
all_jars_classpath := $(subst $(NULL) ,:,$(strip $(ALL_JARS)))
|
|
||||||
|
|
||||||
include $(topsrcdir)/config/config.mk
|
|
||||||
|
|
||||||
library_jars := \
|
|
||||||
$(ANDROID_SDK)/android.jar \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
# Android 23 (Marshmallow) deprecated a part of the Android platform, namely the
|
|
||||||
# org.apache.http package. Fennec removed all code that referenced said package
|
|
||||||
# in order to easily ship to Android 23 devices (and, by extension, all devices
|
|
||||||
# before Android 23).
|
|
||||||
#
|
|
||||||
# Google did not remove all code that referenced said package in their own
|
|
||||||
# com.google.android.gms namespace! It turns out that the org.apache.http
|
|
||||||
# package is not removed, only deprecated and hidden by default. Google added a
|
|
||||||
# a `useLibrary` Gradle directive that allows legacy code to reference the
|
|
||||||
# package, which is still (hidden) in the Android 23 platform.
|
|
||||||
#
|
|
||||||
# Fennec code doesn't need to compile against the deprecated package, since our
|
|
||||||
# code doesn't reference the package anymore. However, we do need to Proguard
|
|
||||||
# against the deprecated package. If we don't, Proguard -- which is a global
|
|
||||||
# optimization -- sees Google libraries referencing "non-existent" libraries and
|
|
||||||
# complains. The solution is to mimic the `useLibraries` directive by declaring
|
|
||||||
# the legacy package as a provided library jar.
|
|
||||||
#
|
|
||||||
# See https://bugzilla.mozilla.org/show_bug.cgi?id=1233238#c19 for symptoms and
|
|
||||||
# more discussion.
|
|
||||||
ifdef MOZ_INSTALL_TRACKING
|
|
||||||
library_jars += $(ANDROID_SDK)/optional/org.apache.http.legacy.jar
|
|
||||||
endif # MOZ_INSTALL_TRACKING
|
|
||||||
|
|
||||||
library_jars := $(subst $(NULL) ,:,$(strip $(library_jars)))
|
|
||||||
|
|
||||||
gradle_dir := $(topobjdir)/gradle/build/mobile/android
|
|
||||||
|
|
||||||
ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
|
|
||||||
.gradle.deps: .aapt.deps FORCE
|
|
||||||
@$(TOUCH) $@
|
|
||||||
$(topsrcdir)/mach gradle \
|
|
||||||
app:assembleAutomationDebug app:assembleAutomationDebugAndroidTest -x lint
|
|
||||||
|
|
||||||
classes.dex: .gradle.deps
|
|
||||||
$(REPORT_BUILD)
|
|
||||||
cp $(gradle_dir)/app/intermediates/transforms/dex/automation/debug/folders/1000/1f/main/classes.dex $@
|
|
||||||
else
|
|
||||||
classes.dex: .proguard.deps
|
|
||||||
$(REPORT_BUILD)
|
|
||||||
$(DX) --dex --output=classes.dex jars-proguarded
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifdef MOZ_DISABLE_PROGUARD
|
|
||||||
PROGUARD_PASSES=0
|
|
||||||
else
|
|
||||||
ifdef MOZ_DEBUG
|
|
||||||
PROGUARD_PASSES=1
|
|
||||||
else
|
|
||||||
ifndef MOZILLA_OFFICIAL
|
|
||||||
PROGUARD_PASSES=1
|
|
||||||
else
|
|
||||||
PROGUARD_PASSES=6
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
proguard_config_dir=$(topsrcdir)/mobile/android/config/proguard
|
|
||||||
|
|
||||||
# This stanza ensures that the set of GeckoView classes does not depend on too
|
|
||||||
# much of Fennec, where "too much" is defined as the set of potentially
|
|
||||||
# non-GeckoView classes that GeckoView already depended on at a certain point in
|
|
||||||
# time. The idea is to set a high-water mark that is not to be crossed.
|
|
||||||
classycle_jar := $(topsrcdir)/mobile/android/build/classycle/classycle-1.4.1.jar
|
|
||||||
.geckoview.deps: geckoview.ddf $(classycle_jar) $(ALL_JARS)
|
|
||||||
$(JAVA) -cp $(classycle_jar) \
|
|
||||||
classycle.dependency.DependencyChecker \
|
|
||||||
-mergeInnerClasses \
|
|
||||||
-dependencies=@$< \
|
|
||||||
$(ALL_JARS)
|
|
||||||
@$(TOUCH) $@
|
|
||||||
|
|
||||||
# First, we delete debugging information from libraries. Having line-number
|
|
||||||
# information for libraries for which we lack the source isn't useful, so this
|
|
||||||
# saves us a bit of space. Importantly, Proguard has a bug causing it to
|
|
||||||
# sometimes corrupt this information if present (which it does for some of the
|
|
||||||
# included libraries). This corruption prevents dex from completing, so we need
|
|
||||||
# to get rid of it. This prevents us from seeing line numbers in stack traces
|
|
||||||
# for stack frames inside libraries.
|
|
||||||
#
|
|
||||||
# This step can occur much earlier than the main Proguard pass: it needs only
|
|
||||||
# gecko-R.jar to have been compiled (as that's where the library R.java files
|
|
||||||
# end up), but it does block the main Proguard pass.
|
|
||||||
.bundled.proguard.deps: gecko-R.jar $(proguard_config_dir)/strip-libs.cfg
|
|
||||||
$(REPORT_BUILD)
|
|
||||||
@$(TOUCH) $@
|
|
||||||
$(JAVA) \
|
|
||||||
-Xmx512m -Xms128m \
|
|
||||||
-jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \
|
|
||||||
@$(proguard_config_dir)/strip-libs.cfg \
|
|
||||||
-injars $(subst ::,:,$(java_bundled_libs))\
|
|
||||||
-outjars bundled-jars-nodebug \
|
|
||||||
-libraryjars $(library_jars):gecko-R.jar
|
|
||||||
|
|
||||||
# We touch the target file before invoking Proguard so that Proguard's
|
|
||||||
# outputs are fresher than the target, preventing a subsequent
|
|
||||||
# invocation from thinking Proguard's outputs are stale. This is safe
|
|
||||||
# because Make removes the target file if any recipe command fails.
|
|
||||||
.proguard.deps: .geckoview.deps .bundled.proguard.deps $(ALL_JARS) $(proguard_config_dir)/proguard.cfg
|
|
||||||
$(REPORT_BUILD)
|
|
||||||
@$(TOUCH) $@
|
|
||||||
$(JAVA) \
|
|
||||||
-Xmx512m -Xms128m \
|
|
||||||
-jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \
|
|
||||||
@$(proguard_config_dir)/proguard.cfg \
|
|
||||||
-optimizationpasses $(PROGUARD_PASSES) \
|
|
||||||
-injars $(subst ::,:,$(all_jars_classpath)):bundled-jars-nodebug \
|
|
||||||
-outjars jars-proguarded \
|
|
||||||
-libraryjars $(library_jars)
|
|
||||||
|
|
||||||
ANNOTATION_PROCESSOR_JAR_FILES := $(DEPTH)/build/annotationProcessors/annotationProcessors.jar
|
|
||||||
|
|
||||||
# This annotation processing step also generates
|
|
||||||
# GeneratedJNIWrappers.h and GeneratedJNINatives.h
|
|
||||||
GeneratedJNIWrappers.cpp: $(ANNOTATION_PROCESSOR_JAR_FILES) $(GECKOVIEW_JARS)
|
|
||||||
$(JAVA) -classpath $(geckoview_jars_classpath):$(JAVA_BOOTCLASSPATH):$(JAVA_CLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) \
|
|
||||||
org.mozilla.gecko.annotationProcessors.AnnotationProcessor \
|
|
||||||
Generated $(GECKOVIEW_JARS)
|
|
||||||
|
|
||||||
# This annotation processing step also generates
|
|
||||||
# FennecJNIWrappers.h and FennecJNINatives.h
|
|
||||||
FennecJNIWrappers.cpp: $(ANNOTATION_PROCESSOR_JAR_FILES) $(FENNEC_JARS)
|
|
||||||
$(JAVA) -classpath $(all_jars_classpath):$(JAVA_BOOTCLASSPATH):$(JAVA_CLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) \
|
|
||||||
org.mozilla.gecko.annotationProcessors.AnnotationProcessor \
|
|
||||||
Fennec $(FENNEC_JARS)
|
|
||||||
|
|
||||||
# Certain source files need to be preprocessed. This special rule
|
|
||||||
# generates these files into generated/org/mozilla/gecko for
|
|
||||||
# consumption by the build system and IDEs.
|
|
||||||
|
|
||||||
# The list in moz.build looks like
|
|
||||||
# 'preprocessed/org/mozilla/gecko/AppConstants.java'. The list in
|
|
||||||
# constants_PP_JAVAFILES looks like
|
|
||||||
# 'generated/preprocessed/org/mozilla/gecko/AppConstants.java'. We
|
|
||||||
# need to write AppConstants.java.in to
|
|
||||||
# generated/preprocessed/org/mozilla/gecko.
|
|
||||||
preprocessed := $(addsuffix .in,$(subst generated/preprocessed/org/mozilla/gecko/,,$(filter generated/preprocessed/org/mozilla/gecko/%,$(constants_PP_JAVAFILES))))
|
|
||||||
|
|
||||||
preprocessed_PATH := generated/preprocessed/org/mozilla/gecko
|
|
||||||
preprocessed_KEEP_PATH := 1
|
|
||||||
preprocessed_FLAGS := --marker='//\\\#'
|
|
||||||
|
|
||||||
PP_TARGETS += preprocessed
|
|
||||||
|
|
||||||
include $(topsrcdir)/config/rules.mk
|
|
||||||
|
|
||||||
not_android_res_files := \
|
|
||||||
*.mkdir.done* \
|
|
||||||
*.DS_Store* \
|
|
||||||
*\#* \
|
|
||||||
*.rej \
|
|
||||||
*.orig \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
# This uses the fact that Android resource directories list all
|
|
||||||
# resource files one subdirectory below the parent resource directory.
|
|
||||||
android_res_files := $(filter-out $(not_android_res_files),$(wildcard $(addsuffix /*,$(wildcard $(addsuffix /*,$(ANDROID_RES_DIRS))))))
|
|
||||||
|
|
||||||
$(ANDROID_GENERATED_RESFILES): $(call mkdir_deps,$(sort $(dir $(ANDROID_GENERATED_RESFILES))))
|
|
||||||
|
|
||||||
# [Comment 1/3] We don't have correct dependencies for strings.xml at
|
|
||||||
# this point, so we always recursively invoke the submake to check the
|
|
||||||
# dependencies. Sigh. And, with multilocale builds, there will be
|
|
||||||
# multiple strings.xml files, and we need to rebuild gecko.ap_ if any
|
|
||||||
# of them change. But! mobile/android/base/locales does not have
|
|
||||||
# enough information to actually build res/values/strings.xml during a
|
|
||||||
# language repack. So rather than adding rules into the main
|
|
||||||
# makefile, and trying to work around the lack of information, we
|
|
||||||
# force a rebuild of gecko.ap_ during packaging. See below.
|
|
||||||
|
|
||||||
# Since the sub-Make is forced, it doesn't matter that we touch the
|
|
||||||
# target file before the command. If in the future we stop forcing
|
|
||||||
# the sub-Make, touching the target file first is better, because the
|
|
||||||
# sub-Make outputs will be fresher than the target, and not require
|
|
||||||
# rebuilding. This is all safe because Make removes the target file
|
|
||||||
# if any recipe command fails. It is crucial that the sub-Make touch
|
|
||||||
# the target files (those depending on .locales.deps) only when there
|
|
||||||
# contents have changed; otherwise, this will force rebuild them as
|
|
||||||
# part of every build.
|
|
||||||
.locales.deps: FORCE
|
|
||||||
$(TOUCH) $@
|
|
||||||
$(MAKE) -C locales
|
|
||||||
|
|
||||||
|
|
||||||
# This .deps pattern saves an invocation of the sub-Make: the single
|
|
||||||
# invocation generates strings.xml, browsersearch.json, and
|
|
||||||
# suggestedsites.json. The trailing semi-colon defines an empty
|
|
||||||
# recipe: defining no recipe at all causes Make to treat the target
|
|
||||||
# differently, in a way that defeats our dependencies.
|
|
||||||
res/values/strings.xml: .locales.deps ;
|
|
||||||
res/raw/browsersearch.json: .locales.deps ;
|
|
||||||
res/raw/suggestedsites.json: .locales.deps ;
|
|
||||||
|
|
||||||
all_resources = \
|
|
||||||
$(DEPTH)/mobile/android/base/AndroidManifest.xml \
|
|
||||||
$(android_res_files) \
|
|
||||||
$(ANDROID_GENERATED_RESFILES) \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
# All of generated/org/mozilla/gecko/R.java, gecko.ap_, and R.txt are
|
|
||||||
# produced by aapt; this saves aapt invocations. The trailing
|
|
||||||
# semi-colon defines an empty recipe; defining no recipe at all causes
|
|
||||||
# Make to treat the target differently, in a way that defeats our
|
|
||||||
# dependencies.
|
|
||||||
|
|
||||||
generated/org/mozilla/gecko/R.java: .aapt.deps ;
|
|
||||||
|
|
||||||
# Only add libraries that contain resources here. We (unecessarily) generate an identical R.java which
|
|
||||||
# is copied into each of these locations, and each of these files contains thousands of fields.
|
|
||||||
# Each unnecessary copy therefore wastes unnecessary fields in the output dex file.
|
|
||||||
# Note: usually proguard will help clean this up after the fact, but having too many fields will cause
|
|
||||||
# dexing to fail, regardless of any later optimisations proguard could later make to bring us back
|
|
||||||
# under the limit.
|
|
||||||
# Ideally we would fix our aapt invocations to correctly generate minimal copies of R.java for each
|
|
||||||
# package, but that seems redundant since gradle builds are able to correctly generate these files.
|
|
||||||
|
|
||||||
# If native devices are enabled, add Google Play Services, build their resources
|
|
||||||
# (no resources) generated/android/support/v4/R.java: .aapt.deps ;
|
|
||||||
generated/android/support/v7/appcompat/R.java: .aapt.deps ;
|
|
||||||
# (no resources) generated/android/support/graphics/drawable/animated/R.java: .aapt.deps ;
|
|
||||||
# (no resources) generated/android/support/graphics/drawable/R.java: .aapt.deps ;
|
|
||||||
generated/android/support/v7/cardview/R.java: .aapt.deps ;
|
|
||||||
generated/android/support/design/R.java: .aapt.deps ;
|
|
||||||
generated/android/support/v7/mediarouter/R.java: .aapt.deps ;
|
|
||||||
generated/android/support/v7/recyclerview/R.java: .aapt.deps ;
|
|
||||||
# (no resources) generated/android/support/customtabs/R.java: .aapt.deps ;
|
|
||||||
# (no resources) generated/android/support/v7/palette/R.java: .aapt.deps ;
|
|
||||||
generated/com/google/android/gms/R.java: .aapt.deps ;
|
|
||||||
generated/com/google/android/gms/ads/R.java: .aapt.deps ;
|
|
||||||
generated/com/google/android/gms/base/R.java: .aapt.deps ;
|
|
||||||
generated/com/google/android/gms/cast/R.java: .aapt.deps ;
|
|
||||||
# (no resources) generated/com/google/android/gms/gcm/R.java: .aapt.deps ;
|
|
||||||
# (no resources) generated/com/google/android/gms/measurement/R.java: .aapt.deps ;
|
|
||||||
|
|
||||||
gecko.ap_: .aapt.deps ;
|
|
||||||
R.txt: .aapt.deps ;
|
|
||||||
|
|
||||||
# [Comment 2/3] This tom-foolery provides a target that forces a
|
|
||||||
# rebuild of gecko.ap_. This is used during packaging to ensure that
|
|
||||||
# resources are fresh. The alternative would be complicated; see
|
|
||||||
# [Comment 1/3].
|
|
||||||
|
|
||||||
gecko-nodeps/R.java: .aapt.nodeps ;
|
|
||||||
gecko-nodeps.ap_: .aapt.nodeps ;
|
|
||||||
gecko-nodeps/R.txt: .aapt.nodeps ;
|
|
||||||
|
|
||||||
# This ignores the default set of resources ignored by aapt, plus
|
|
||||||
# files starting with '#'. (Emacs produces temp files named #temp#.)
|
|
||||||
# This doesn't actually set the environment variable; it's used as a
|
|
||||||
# parameter in the aapt invocation below. Consider updating
|
|
||||||
# not_android_res_files as well.
|
|
||||||
|
|
||||||
ANDROID_AAPT_IGNORE := !.svn:!.git:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*.scc:*~:\#*:*.rej:*.orig
|
|
||||||
|
|
||||||
# 1: target file.
|
|
||||||
# 2: dependencies.
|
|
||||||
# 3: name of ap_ file to write.
|
|
||||||
# 4: directory to write R.java into.
|
|
||||||
# 5: directory to write R.txt into.
|
|
||||||
# We touch the target file before invoking aapt so that aapt's outputs
|
|
||||||
# are fresher than the target, preventing a subsequent invocation from
|
|
||||||
# thinking aapt's outputs are stale. This is safe because Make
|
|
||||||
# removes the target file if any recipe command fails.
|
|
||||||
|
|
||||||
define aapt_command
|
|
||||||
$(1): $$(call mkdir_deps,$(filter-out ./,$(dir $(3) $(4) $(5)))) $(2)
|
|
||||||
@$$(TOUCH) $$@
|
|
||||||
$$(AAPT) package -f -m \
|
|
||||||
-M AndroidManifest.xml \
|
|
||||||
-I $(ANDROID_SDK)/android.jar \
|
|
||||||
$(if $(MOZ_ANDROID_MAX_SDK_VERSION),--max-res-version $(MOZ_ANDROID_MAX_SDK_VERSION),) \
|
|
||||||
--auto-add-overlay \
|
|
||||||
$$(addprefix -S ,$$(ANDROID_RES_DIRS)) \
|
|
||||||
$$(addprefix -A ,$$(ANDROID_ASSETS_DIRS)) \
|
|
||||||
$(if $(ANDROID_EXTRA_PACKAGES),--extra-packages $$(subst $$(NULL) ,:,$$(strip $$(ANDROID_EXTRA_PACKAGES)))) \
|
|
||||||
$(if $(ANDROID_EXTRA_RES_DIRS),$$(addprefix -S ,$$(ANDROID_EXTRA_RES_DIRS))) \
|
|
||||||
--custom-package org.mozilla.gecko \
|
|
||||||
--no-version-vectors \
|
|
||||||
-F $(3) \
|
|
||||||
-J $(4) \
|
|
||||||
--output-text-symbols $(5) \
|
|
||||||
--ignore-assets "$$(ANDROID_AAPT_IGNORE)"
|
|
||||||
endef
|
|
||||||
|
|
||||||
# [Comment 3/3] The first of these rules is used during regular
|
|
||||||
# builds. The second writes an ap_ file that is only used during
|
|
||||||
# packaging. It doesn't write the normal ap_, or R.java, since we
|
|
||||||
# don't want the packaging step to write anything that would make a
|
|
||||||
# further no-op build do work. See also
|
|
||||||
# toolkit/mozapps/installer/packager.mk.
|
|
||||||
|
|
||||||
# .aapt.deps: $(all_resources)
|
|
||||||
$(eval $(call aapt_command,.aapt.deps,$(all_resources),gecko.ap_,generated/,./))
|
|
||||||
|
|
||||||
ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
|
|
||||||
.aapt.nodeps: FORCE
|
|
||||||
cp $(gradle_dir)/app/intermediates/res/resources-automation-debug.ap_ gecko-nodeps.ap_
|
|
||||||
else
|
|
||||||
# .aapt.nodeps: $(DEPTH)/mobile/android/base/AndroidManifest.xml FORCE
|
|
||||||
$(eval $(call aapt_command,.aapt.nodeps,$(DEPTH)/mobile/android/base/AndroidManifest.xml FORCE,gecko-nodeps.ap_,gecko-nodeps/,gecko-nodeps/))
|
|
||||||
endif
|
|
||||||
|
|
||||||
# Override the Java settings with some specific android settings
|
|
||||||
include $(topsrcdir)/config/android-common.mk
|
|
||||||
|
|
||||||
update-generated-wrappers:
|
|
||||||
@cp $(CURDIR)/GeneratedJNIWrappers.cpp \
|
|
||||||
$(CURDIR)/GeneratedJNIWrappers.h \
|
|
||||||
$(CURDIR)/GeneratedJNINatives.h $(topsrcdir)/widget/android
|
|
||||||
@echo Updated generated JNI code
|
|
||||||
|
|
||||||
update-fennec-wrappers:
|
|
||||||
@cp $(CURDIR)/FennecJNIWrappers.cpp \
|
|
||||||
$(CURDIR)/FennecJNIWrappers.h \
|
|
||||||
$(CURDIR)/FennecJNINatives.h $(topsrcdir)/widget/android/fennec
|
|
||||||
@echo Updated Fennec JNI code
|
|
||||||
|
|
||||||
.PHONY: update-generated-wrappers
|
|
||||||
|
|
||||||
# This target is only used by IDE integrations. It rebuilds resources
|
|
||||||
# that end up in omni.ja using the equivalent of |mach build faster|,
|
|
||||||
# does most of the packaging step, and then updates omni.ja in
|
|
||||||
# place. If you're not using an IDE, you should be using |mach build
|
|
||||||
# mobile/android && mach package|.
|
|
||||||
$(ABS_DIST)/fennec/$(OMNIJAR_NAME): FORCE
|
|
||||||
$(REPORT_BUILD)
|
|
||||||
$(MAKE) -C ../../../faster
|
|
||||||
$(MAKE) -C ../installer stage-package
|
|
||||||
$(MKDIR) -p $(@D)
|
|
||||||
rsync --update $(DIST)/fennec/$(notdir $(OMNIJAR_NAME)) $@
|
|
||||||
$(RM) $(DIST)/fennec/$(notdir $(OMNIJAR_NAME))
|
|
||||||
|
|
||||||
# Targets built very early during a Gradle build.
|
|
||||||
gradle-targets: $(foreach f,$(constants_PP_JAVAFILES),$(f))
|
|
||||||
gradle-targets: $(DEPTH)/mobile/android/base/AndroidManifest.xml
|
|
||||||
gradle-targets: $(ANDROID_GENERATED_RESFILES)
|
|
||||||
|
|
||||||
ifndef MOZILLA_OFFICIAL
|
|
||||||
# Local developers update omni.ja during their builds. There's a
|
|
||||||
# chicken-and-egg problem here.
|
|
||||||
gradle-omnijar: $(abspath $(DIST)/fennec/$(OMNIJAR_NAME))
|
|
||||||
else
|
|
||||||
# In automation, omni.ja is built only during packaging.
|
|
||||||
gradle-omnijar:
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: gradle-targets gradle-omnijar
|
|
||||||
|
|
||||||
# GeneratedJNIWrappers.cpp target also generates
|
|
||||||
# GeneratedJNIWrappers.h and GeneratedJNINatives.h
|
|
||||||
# FennecJNIWrappers.cpp target also generates
|
|
||||||
# FennecJNIWrappers.h and FennecJNINatives.h
|
|
||||||
ifndef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
|
|
||||||
libs:: GeneratedJNIWrappers.cpp
|
|
||||||
@(diff GeneratedJNIWrappers.cpp $(topsrcdir)/widget/android/GeneratedJNIWrappers.cpp >/dev/null && \
|
|
||||||
diff GeneratedJNIWrappers.h $(topsrcdir)/widget/android/GeneratedJNIWrappers.h >/dev/null && \
|
|
||||||
diff GeneratedJNINatives.h $(topsrcdir)/widget/android/GeneratedJNINatives.h >/dev/null) || \
|
|
||||||
(echo '*****************************************************' && \
|
|
||||||
echo '*** Error: The generated JNI code has changed ***' && \
|
|
||||||
echo '* To update generated code in the tree, please run *' && \
|
|
||||||
echo && \
|
|
||||||
echo ' make -C $(CURDIR) update-generated-wrappers' && \
|
|
||||||
echo && \
|
|
||||||
echo '* Repeat the build, and check in any changes. *' && \
|
|
||||||
echo '*****************************************************' && \
|
|
||||||
exit 1)
|
|
||||||
|
|
||||||
libs:: FennecJNIWrappers.cpp
|
|
||||||
@(diff FennecJNIWrappers.cpp $(topsrcdir)/widget/android/fennec/FennecJNIWrappers.cpp >/dev/null && \
|
|
||||||
diff FennecJNIWrappers.h $(topsrcdir)/widget/android/fennec/FennecJNIWrappers.h >/dev/null && \
|
|
||||||
diff FennecJNINatives.h $(topsrcdir)/widget/android/fennec/FennecJNINatives.h >/dev/null) || \
|
|
||||||
(echo '*****************************************************' && \
|
|
||||||
echo '*** Error: The Fennec JNI code has changed ***' && \
|
|
||||||
echo '* To update generated code in the tree, please run *' && \
|
|
||||||
echo && \
|
|
||||||
echo ' make -C $(CURDIR) update-fennec-wrappers' && \
|
|
||||||
echo && \
|
|
||||||
echo '* Repeat the build, and check in any changes. *' && \
|
|
||||||
echo '*****************************************************' && \
|
|
||||||
exit 1)
|
|
||||||
endif
|
|
||||||
|
|
||||||
libs:: classes.dex
|
|
||||||
$(INSTALL) classes.dex $(FINAL_TARGET)
|
|
||||||
|
|
||||||
# Generate Java binder interfaces from AIDL files.
|
|
||||||
aidl_src_path := $(srcdir)/aidl
|
|
||||||
aidl_target_path := generated
|
|
||||||
media_pkg := org/mozilla/gecko/media
|
|
||||||
|
|
||||||
$(aidl_target_path)/$(media_pkg)/%.java:$(aidl_src_path)/$(media_pkg)/%.aidl
|
|
||||||
@echo "Processing AIDL: $< => $@"
|
|
||||||
$(AIDL) -p$(ANDROID_SDK)/framework.aidl -I$(aidl_src_path) -o$(aidl_target_path) $<
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ABCDEFGHIJKL
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
//#ifdef MOZ_INSTALL_TRACKING
|
|
||||||
//#define MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN @MOZ_ADJUST_SDK_KEY@
|
|
||||||
//#endif
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
/* 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.media;
|
|
||||||
|
|
||||||
parcelable FormatParam;
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
/* 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.media;
|
|
||||||
|
|
||||||
// Non-default types used in interface.
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Surface;
|
|
||||||
import org.mozilla.gecko.media.FormatParam;
|
|
||||||
import org.mozilla.gecko.media.ICodecCallbacks;
|
|
||||||
import org.mozilla.gecko.media.Sample;
|
|
||||||
|
|
||||||
interface ICodec {
|
|
||||||
void setCallbacks(in ICodecCallbacks callbacks);
|
|
||||||
boolean configure(in FormatParam format, inout Surface surface, int flags);
|
|
||||||
oneway void start();
|
|
||||||
oneway void stop();
|
|
||||||
oneway void flush();
|
|
||||||
oneway void release();
|
|
||||||
|
|
||||||
Sample dequeueInput(int size);
|
|
||||||
oneway void queueInput(in Sample sample);
|
|
||||||
|
|
||||||
oneway void releaseOutput(in Sample sample);
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
/* 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.media;
|
|
||||||
|
|
||||||
// Non-default types used in interface.
|
|
||||||
import org.mozilla.gecko.media.FormatParam;
|
|
||||||
import org.mozilla.gecko.media.Sample;
|
|
||||||
|
|
||||||
interface ICodecCallbacks {
|
|
||||||
oneway void onInputExhausted();
|
|
||||||
oneway void onOutputFormatChanged(in FormatParam format);
|
|
||||||
oneway void onOutput(in Sample sample);
|
|
||||||
oneway void onError(boolean fatal);
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/* 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.media;
|
|
||||||
|
|
||||||
// Non-default types used in interface.
|
|
||||||
import org.mozilla.gecko.media.IMediaDrmBridgeCallbacks;
|
|
||||||
|
|
||||||
interface IMediaDrmBridge {
|
|
||||||
void setCallbacks(in IMediaDrmBridgeCallbacks callbacks);
|
|
||||||
|
|
||||||
oneway void createSession(int createSessionToken,
|
|
||||||
int promiseId,
|
|
||||||
String initDataType,
|
|
||||||
in byte[] initData);
|
|
||||||
|
|
||||||
oneway void updateSession(int promiseId,
|
|
||||||
String sessionId,
|
|
||||||
in byte[] response);
|
|
||||||
|
|
||||||
oneway void closeSession(int promiseId, String sessionId);
|
|
||||||
|
|
||||||
oneway void release();
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
/* 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.media;
|
|
||||||
|
|
||||||
// Non-default types used in interface.
|
|
||||||
import org.mozilla.gecko.media.SessionKeyInfo;
|
|
||||||
|
|
||||||
interface IMediaDrmBridgeCallbacks {
|
|
||||||
|
|
||||||
oneway void onSessionCreated(int createSessionToken,
|
|
||||||
int promiseId,
|
|
||||||
in byte[] sessionId,
|
|
||||||
in byte[] request);
|
|
||||||
|
|
||||||
oneway void onSessionUpdated(int promiseId, in byte[] sessionId);
|
|
||||||
|
|
||||||
oneway void onSessionClosed(int promiseId, in byte[] sessionId);
|
|
||||||
|
|
||||||
oneway void onSessionMessage(in byte[] sessionId,
|
|
||||||
int sessionMessageType,
|
|
||||||
in byte[] request);
|
|
||||||
|
|
||||||
oneway void onSessionError(in byte[] sessionId, String message);
|
|
||||||
|
|
||||||
oneway void onSessionBatchedKeyChanged(in byte[] sessionId,
|
|
||||||
in SessionKeyInfo[] keyInfos);
|
|
||||||
|
|
||||||
oneway void onRejectPromise(int promiseId, String message);
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/* 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.media;
|
|
||||||
|
|
||||||
// Non-default types used in interface.
|
|
||||||
import org.mozilla.gecko.media.ICodec;
|
|
||||||
import org.mozilla.gecko.media.IMediaDrmBridge;
|
|
||||||
|
|
||||||
interface IMediaManager {
|
|
||||||
/** Creates a remote ICodec object. */
|
|
||||||
ICodec createCodec();
|
|
||||||
|
|
||||||
/** Creates a remote IMediaDrmBridge object. */
|
|
||||||
IMediaDrmBridge createRemoteMediaDrmBridge(in String keySystem,
|
|
||||||
in String stubId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
/* 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.media;
|
|
||||||
|
|
||||||
parcelable Sample;
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
/* 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.media;
|
|
||||||
|
|
||||||
parcelable SessionKeyInfo;
|
|
||||||
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
|
|
@ -1,22 +0,0 @@
|
||||||
<?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/. -->
|
|
||||||
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<item android:state_enabled="true">
|
|
||||||
<shape>
|
|
||||||
<solid android:color="@color/textbox_background" />
|
|
||||||
<stroke android:width="1dp" android:color="@color/textbox_stroke" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<item android:state_enabled="false">
|
|
||||||
<shape>
|
|
||||||
<solid android:color="@color/textbox_background_disabled" />
|
|
||||||
<stroke android:width="1dp" android:color="@color/textbox_stroke_disabled" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
</selector>
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
<?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/. -->
|
|
||||||
|
|
||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fillViewport="true">
|
|
||||||
|
|
||||||
<LinearLayout android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:background="@color/toolbar_grey">
|
|
||||||
|
|
||||||
<LinearLayout android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="10dp"
|
|
||||||
android:layout_weight="1">
|
|
||||||
|
|
||||||
<TextView android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="10dip"
|
|
||||||
android:textSize="30sp"
|
|
||||||
android:textColor="#000"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:fontFamily="sans-serif-light"
|
|
||||||
android:text="@string/crash_sorry"/>
|
|
||||||
|
|
||||||
<TextView android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="10dip"
|
|
||||||
android:textAppearance="@style/TextAppearance"
|
|
||||||
android:text="@string/crash_message2"/>
|
|
||||||
|
|
||||||
<CheckBox android:id="@+id/send_report"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:checked="true"
|
|
||||||
android:textColor="@color/primary_text"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:text="@string/crash_send_report_message3"/>
|
|
||||||
|
|
||||||
<EditText android:id="@+id/comment"
|
|
||||||
style="@style/CrashReporter.EditText"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ems="10"
|
|
||||||
android:inputType="textMultiLine"
|
|
||||||
android:lines="5"
|
|
||||||
android:gravity="top"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:hint="@string/crash_comment" />
|
|
||||||
|
|
||||||
<CheckBox android:id="@+id/include_url"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textColor="@color/primary_text"
|
|
||||||
android:textAppearance="@style/TextAppearance"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:text="@string/crash_include_url2"/>
|
|
||||||
|
|
||||||
<CheckBox android:id="@+id/allow_contact"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textColor="@color/primary_text"
|
|
||||||
android:textAppearance="@style/TextAppearance"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:text="@string/crash_allow_contact2"/>
|
|
||||||
|
|
||||||
<org.mozilla.gecko.widget.ClickableWhenDisabledEditText
|
|
||||||
android:id="@+id/email"
|
|
||||||
style="@style/CrashReporter.EditText"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ems="10"
|
|
||||||
android:inputType="textEmailAddress"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:enabled="false"
|
|
||||||
android:clickable="true"
|
|
||||||
android:hint="@string/crash_email" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<View android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="#999" />
|
|
||||||
|
|
||||||
<LinearLayout android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_gravity="bottom">
|
|
||||||
|
|
||||||
<Button android:id="@+id/close"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1.0"
|
|
||||||
android:padding="15dp"
|
|
||||||
android:onClick="onCloseClick"
|
|
||||||
android:text="@string/crash_close_label"
|
|
||||||
android:textAppearance="?android:attr/textAppearance"
|
|
||||||
android:background="@drawable/action_bar_button"/>
|
|
||||||
|
|
||||||
<View android:layout_width="1dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="#999" />
|
|
||||||
|
|
||||||
<Button android:id="@+id/restart"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1.0"
|
|
||||||
android:padding="15dp"
|
|
||||||
android:onClick="onRestartClick"
|
|
||||||
android:text="@string/crash_restart_label"
|
|
||||||
android:textAppearance="?android:attr/textAppearance"
|
|
||||||
android:background="@drawable/action_bar_button"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?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/. -->
|
|
||||||
|
|
||||||
<resources>
|
|
||||||
<!-- Crash Reporter Colors -->
|
|
||||||
<color name="textbox_background">#FFF</color>
|
|
||||||
<color name="textbox_background_disabled">#DDD</color>
|
|
||||||
<color name="textbox_stroke">#000</color>
|
|
||||||
<color name="textbox_stroke_disabled">#666</color>
|
|
||||||
</resources>
|
|
||||||
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<?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/. -->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Crash Reporter Styles -->
|
|
||||||
<style name="CrashReporter" />
|
|
||||||
|
|
||||||
<style name="CrashReporter.EditText">
|
|
||||||
<item name="android:background">@drawable/textbox_bg</item>
|
|
||||||
<item name="android:padding">10dp</item>
|
|
||||||
<item name="android:textAppearance">@style/TextAppearance</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
# This is a Classycle dependency definition file that asserts that the contents
|
|
||||||
# of the GeckoView library (Classycle set [lib]) is a dependency (but does not
|
|
||||||
# depend) on Fennec (Classycle set [main]). The additional Classycle set
|
|
||||||
# [middle] consists of classes referenced by GeckoView that probably should not
|
|
||||||
# be referenced. We want this middle set to shrink over time.
|
|
||||||
|
|
||||||
show allResults
|
|
||||||
|
|
||||||
[lib] = \
|
|
||||||
org.mozilla.gecko.gfx.* \
|
|
||||||
org.mozilla.gecko.mozglue.* \
|
|
||||||
org.mozilla.gecko.sqlite.* \
|
|
||||||
org.mozilla.gecko.util.* \
|
|
||||||
org.mozilla.gecko.AndroidGamepadManager \
|
|
||||||
org.mozilla.gecko.AppConstants \
|
|
||||||
org.mozilla.gecko.BaseGeckoInterface \
|
|
||||||
org.mozilla.gecko.ContextGetter \
|
|
||||||
org.mozilla.gecko.CrashHandler \
|
|
||||||
org.mozilla.gecko.EventDispatcher \
|
|
||||||
org.mozilla.gecko.GeckoAccessibility \
|
|
||||||
org.mozilla.gecko.GeckoAppShell \
|
|
||||||
org.mozilla.gecko.GeckoBatteryManager \
|
|
||||||
org.mozilla.gecko.GeckoEditable \
|
|
||||||
org.mozilla.gecko.GeckoEditableClient \
|
|
||||||
org.mozilla.gecko.GeckoEditableListener \
|
|
||||||
org.mozilla.gecko.GeckoEvent \
|
|
||||||
org.mozilla.gecko.GeckoInputConnection \
|
|
||||||
org.mozilla.gecko.GeckoJavaSampler \
|
|
||||||
org.mozilla.gecko.GeckoNetworkManager \
|
|
||||||
org.mozilla.gecko.GeckoProfile \
|
|
||||||
org.mozilla.gecko.GeckoScreenOrientation \
|
|
||||||
org.mozilla.gecko.GeckoSharedPrefs \
|
|
||||||
org.mozilla.gecko.GeckoThread \
|
|
||||||
org.mozilla.gecko.GeckoView \
|
|
||||||
org.mozilla.gecko.GlobalHistory \
|
|
||||||
org.mozilla.gecko.InputMethods \
|
|
||||||
org.mozilla.gecko.NSSBridge \
|
|
||||||
org.mozilla.gecko.NotificationClient \
|
|
||||||
org.mozilla.gecko.NotificationHandler \
|
|
||||||
org.mozilla.gecko.PrefsHelper \
|
|
||||||
org.mozilla.gecko.SysInfo \
|
|
||||||
org.mozilla.gecko.TouchEventInterceptor \
|
|
||||||
org.mozilla.gecko.ZoomConstraints
|
|
||||||
|
|
||||||
[middle] = \
|
|
||||||
org.mozilla.gecko.prompts.* \
|
|
||||||
org.mozilla.gecko.FormAssistPopup \
|
|
||||||
org.mozilla.gecko.GeckoActivity \
|
|
||||||
org.mozilla.gecko.GeckoApp \
|
|
||||||
org.mozilla.gecko.GeckoProfileDirectories \
|
|
||||||
org.mozilla.gecko.GuestSession \
|
|
||||||
org.mozilla.gecko.R \
|
|
||||||
org.mozilla.gecko.Tab \
|
|
||||||
org.mozilla.gecko.Tabs \
|
|
||||||
org.mozilla.gecko.Telemetry \
|
|
||||||
org.mozilla.gecko.TelemetryContract \
|
|
||||||
org.mozilla.gecko.ThumbnailHelper \
|
|
||||||
org.mozilla.gecko.db.BrowserDB \
|
|
||||||
org.mozilla.gecko.db.LocalBrowserDB \
|
|
||||||
org.mozilla.gecko.distribution.Distribution \
|
|
||||||
org.mozilla.gecko.icons.*
|
|
||||||
|
|
||||||
[main] = org.mozilla.gecko.* excluding [lib] [middle]
|
|
||||||
|
|
||||||
check sets [lib] [middle] [main]
|
|
||||||
|
|
||||||
# Bug 1107134: it appears that Classycle can be fooled if the Java
|
|
||||||
# compiler inlines a constant from [main] into [lib]. That is, [main]
|
|
||||||
# really does depend on [lib] but Classycle only sees the dependency
|
|
||||||
# with some javac versions. For now, disable the check. Yes, this
|
|
||||||
# processing is useless without this check.
|
|
||||||
# check [lib] directlyIndependentOf [main]
|
|
||||||
|
|
||||||
# This fails; if this passed, GeckoView would be ready to extract from Fennec.
|
|
||||||
# check [lib] independentOf [middle]
|
|
||||||
|
|
@ -1,596 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public final class ANRReporter extends BroadcastReceiver
|
|
||||||
{
|
|
||||||
private static final boolean DEBUG = false;
|
|
||||||
private static final String LOGTAG = "GeckoANRReporter";
|
|
||||||
|
|
||||||
private static final String ANR_ACTION = "android.intent.action.ANR";
|
|
||||||
// Number of lines to search traces.txt to decide whether it's a Gecko ANR
|
|
||||||
private static final int LINES_TO_IDENTIFY_TRACES = 10;
|
|
||||||
// ANRs may happen because of memory pressure,
|
|
||||||
// so don't use up too much memory here
|
|
||||||
// Size of buffer to hold one line of text
|
|
||||||
private static final int TRACES_LINE_SIZE = 100;
|
|
||||||
// Size of block to use when processing traces.txt
|
|
||||||
private static final int TRACES_BLOCK_SIZE = 2000;
|
|
||||||
private static final String TRACES_CHARSET = "utf-8";
|
|
||||||
private static final String PING_CHARSET = "utf-8";
|
|
||||||
|
|
||||||
private static final ANRReporter sInstance = new ANRReporter();
|
|
||||||
private static int sRegisteredCount;
|
|
||||||
private Handler mHandler;
|
|
||||||
private volatile boolean mPendingANR;
|
|
||||||
|
|
||||||
@WrapForJNI
|
|
||||||
private static native boolean requestNativeStack(boolean unwind);
|
|
||||||
@WrapForJNI
|
|
||||||
private static native String getNativeStack();
|
|
||||||
@WrapForJNI
|
|
||||||
private static native void releaseNativeStack();
|
|
||||||
|
|
||||||
public static void register(Context context) {
|
|
||||||
if (sRegisteredCount++ != 0) {
|
|
||||||
// Already registered
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sInstance.start(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unregister() {
|
|
||||||
if (sRegisteredCount == 0) {
|
|
||||||
Log.w(LOGTAG, "register/unregister mismatch");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (--sRegisteredCount != 0) {
|
|
||||||
// Should still be registered
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sInstance.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void start(final Context context) {
|
|
||||||
|
|
||||||
Thread receiverThread = new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Looper.prepare();
|
|
||||||
synchronized (ANRReporter.this) {
|
|
||||||
mHandler = new Handler();
|
|
||||||
ANRReporter.this.notify();
|
|
||||||
}
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "registering receiver");
|
|
||||||
}
|
|
||||||
context.registerReceiver(ANRReporter.this,
|
|
||||||
new IntentFilter(ANR_ACTION),
|
|
||||||
null,
|
|
||||||
mHandler);
|
|
||||||
Looper.loop();
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "unregistering receiver");
|
|
||||||
}
|
|
||||||
context.unregisterReceiver(ANRReporter.this);
|
|
||||||
mHandler = null;
|
|
||||||
}
|
|
||||||
}, LOGTAG);
|
|
||||||
|
|
||||||
receiverThread.setDaemon(true);
|
|
||||||
receiverThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stop() {
|
|
||||||
synchronized (this) {
|
|
||||||
while (mHandler == null) {
|
|
||||||
try {
|
|
||||||
wait(1000);
|
|
||||||
if (mHandler == null) {
|
|
||||||
// We timed out; just give up. The process is probably
|
|
||||||
// quitting anyways, so we let the OS do the clean up
|
|
||||||
Log.w(LOGTAG, "timed out waiting for handler");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Looper looper = mHandler.getLooper();
|
|
||||||
looper.quit();
|
|
||||||
try {
|
|
||||||
looper.getThread().join();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ANRReporter() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the "traces.txt" file, or null if there is no such file
|
|
||||||
private static File getTracesFile() {
|
|
||||||
// Check most common location first.
|
|
||||||
File tracesFile = new File("/data/anr/traces.txt");
|
|
||||||
if (tracesFile.isFile() && tracesFile.canRead()) {
|
|
||||||
return tracesFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the traces file name if we can.
|
|
||||||
try {
|
|
||||||
// getprop [prop-name [default-value]]
|
|
||||||
Process propProc = (new ProcessBuilder())
|
|
||||||
.command("/system/bin/getprop", "dalvik.vm.stack-trace-file")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start();
|
|
||||||
try {
|
|
||||||
BufferedReader buf = new BufferedReader(
|
|
||||||
new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE);
|
|
||||||
String propVal = buf.readLine();
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal));
|
|
||||||
}
|
|
||||||
// getprop can return empty string when the prop value is empty
|
|
||||||
// or prop is undefined, treat both cases the same way
|
|
||||||
if (propVal != null && propVal.length() != 0) {
|
|
||||||
tracesFile = new File(propVal);
|
|
||||||
if (tracesFile.isFile() && tracesFile.canRead()) {
|
|
||||||
return tracesFile;
|
|
||||||
} else if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "cannot access traces file");
|
|
||||||
}
|
|
||||||
} else if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "empty getprop result");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
propProc.destroy();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(LOGTAG, e);
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
Log.w(LOGTAG, e); // Bug 975436
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File getPingFile() {
|
|
||||||
if (GeckoAppShell.getContext() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
|
|
||||||
if (profile == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
File profDir = profile.getDir();
|
|
||||||
if (profDir == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
File pingDir = new File(profDir, "saved-telemetry-pings");
|
|
||||||
pingDir.mkdirs();
|
|
||||||
if (!(pingDir.exists() && pingDir.isDirectory())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new File(pingDir, UUID.randomUUID().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true if the traces file corresponds to a Gecko ANR
|
|
||||||
private static boolean isGeckoTraces(String pkgName, File tracesFile) {
|
|
||||||
try {
|
|
||||||
final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)";
|
|
||||||
// Regex for finding our package name in the traces file
|
|
||||||
Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME);
|
|
||||||
Pattern mangledPattern = null;
|
|
||||||
if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) {
|
|
||||||
mangledPattern = Pattern.compile(Pattern.quote(
|
|
||||||
AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME);
|
|
||||||
}
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "trying to match package: " + pkgName);
|
|
||||||
}
|
|
||||||
BufferedReader traces = new BufferedReader(
|
|
||||||
new FileReader(tracesFile), TRACES_BLOCK_SIZE);
|
|
||||||
try {
|
|
||||||
for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) {
|
|
||||||
String line = traces.readLine();
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "identifying line: " + String.valueOf(line));
|
|
||||||
}
|
|
||||||
if (line == null) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "reached end of traces file");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (pkgPattern.matcher(line).find()) {
|
|
||||||
// traces.txt file contains our package
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (mangledPattern != null && mangledPattern.matcher(line).find()) {
|
|
||||||
// traces.txt file contains our alternate package
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
traces.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// meh, can't even read from it right. just return false
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long getUptimeMins() {
|
|
||||||
|
|
||||||
long uptimeMins = (new File("/proc/self/stat")).lastModified();
|
|
||||||
if (uptimeMins != 0L) {
|
|
||||||
uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L;
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins));
|
|
||||||
}
|
|
||||||
return uptimeMins;
|
|
||||||
}
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "could not get uptime");
|
|
||||||
}
|
|
||||||
return 0L;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
a saved telemetry ping file consists of JSON in the following format,
|
|
||||||
{
|
|
||||||
"reason": "android-anr-report",
|
|
||||||
"slug": "<uuid-string>",
|
|
||||||
"payload": <json-object>
|
|
||||||
}
|
|
||||||
for Android ANR, our JSON payload should look like,
|
|
||||||
{
|
|
||||||
"ver": 1,
|
|
||||||
"simpleMeasurements": {
|
|
||||||
"uptime": <uptime>
|
|
||||||
},
|
|
||||||
"info": {
|
|
||||||
"reason": "android-anr-report",
|
|
||||||
"OS": "Android",
|
|
||||||
...
|
|
||||||
},
|
|
||||||
"androidANR": "...",
|
|
||||||
"androidLogcat": "..."
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
private static int writePingPayload(OutputStream ping,
|
|
||||||
String payload) throws IOException {
|
|
||||||
byte [] data = payload.getBytes(PING_CHARSET);
|
|
||||||
ping.write(data);
|
|
||||||
return data.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void fillPingHeader(OutputStream ping, String slug)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
// ping file header
|
|
||||||
byte [] data = ("{" +
|
|
||||||
"\"reason\":\"android-anr-report\"," +
|
|
||||||
"\"slug\":" + JSONObject.quote(slug) + "," +
|
|
||||||
"\"payload\":").getBytes(PING_CHARSET);
|
|
||||||
ping.write(data);
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
// payload start
|
|
||||||
int size = writePingPayload(ping, ("{" +
|
|
||||||
"\"ver\":1," +
|
|
||||||
"\"simpleMeasurements\":{" +
|
|
||||||
"\"uptime\":" + String.valueOf(getUptimeMins()) +
|
|
||||||
"}," +
|
|
||||||
"\"info\":{" +
|
|
||||||
"\"reason\":\"android-anr-report\"," +
|
|
||||||
"\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," +
|
|
||||||
"\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," +
|
|
||||||
"\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," +
|
|
||||||
"\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION) + "," +
|
|
||||||
"\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," +
|
|
||||||
"\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
|
|
||||||
"\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," +
|
|
||||||
// Technically the platform build ID may be different, but we'll never know
|
|
||||||
"\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
|
|
||||||
"\"locale\":" + JSONObject.quote(Locales.getLanguageTag(Locale.getDefault())) + "," +
|
|
||||||
"\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," +
|
|
||||||
"\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," +
|
|
||||||
"\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," +
|
|
||||||
"\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," +
|
|
||||||
"\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," +
|
|
||||||
"\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," +
|
|
||||||
"\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) +
|
|
||||||
"}," +
|
|
||||||
"\"androidANR\":\""));
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are at the start of ANR data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block is a section of the larger input stream, and we want to find pattern within
|
|
||||||
// the stream. This is straightforward if the entire pattern is within one block;
|
|
||||||
// however, if the pattern spans across two blocks, we have to match both the start of
|
|
||||||
// the pattern in the first block and the end of the pattern in the second block.
|
|
||||||
// * If pattern is found in block, this method returns the index at the end of the
|
|
||||||
// found pattern, which must always be > 0.
|
|
||||||
// * If pattern is not found, it returns 0.
|
|
||||||
// * If the start of the pattern matches the end of the block, it returns a number
|
|
||||||
// < 0, which equals the negated value of how many characters in pattern are already
|
|
||||||
// matched; when processing the next block, this number is passed in through
|
|
||||||
// prevIndex, and the rest of the characters in pattern are matched against the
|
|
||||||
// start of this second block. The method returns value > 0 if the rest of the
|
|
||||||
// characters match, or 0 if they do not.
|
|
||||||
private static int getEndPatternIndex(String block, String pattern, int prevIndex) {
|
|
||||||
if (pattern == null || block.length() < pattern.length()) {
|
|
||||||
// Nothing to do
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (prevIndex < 0) {
|
|
||||||
// Last block ended with a partial start; now match start of block to rest of pattern
|
|
||||||
if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) {
|
|
||||||
// Rest of pattern matches; return index at end of pattern
|
|
||||||
return pattern.length() + prevIndex;
|
|
||||||
}
|
|
||||||
// Not a match; continue with normal search
|
|
||||||
}
|
|
||||||
// Did not find pattern in last block; see if entire pattern is inside this block
|
|
||||||
int index = block.indexOf(pattern);
|
|
||||||
if (index >= 0) {
|
|
||||||
// Found pattern; return index at end of the pattern
|
|
||||||
return index + pattern.length();
|
|
||||||
}
|
|
||||||
// Block does not contain the entire pattern, but see if the end of the block
|
|
||||||
// contains the start of pattern. To do that, we see if block ends with the
|
|
||||||
// first n-1 characters of pattern, the first n-2 characters of pattern, etc.
|
|
||||||
for (index = block.length() - pattern.length() + 1; index < block.length(); index++) {
|
|
||||||
// Using index as a start, see if the rest of block contains the start of pattern
|
|
||||||
if (block.charAt(index) == pattern.charAt(0) &&
|
|
||||||
block.endsWith(pattern.substring(0, block.length() - index))) {
|
|
||||||
// Found partial match; return -(number of characters matched),
|
|
||||||
// i.e. -1 for 1 character matched, -2 for 2 characters matched, etc.
|
|
||||||
return index - block.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the content of reader to ping;
|
|
||||||
// copying stops when endPattern is found in the input stream
|
|
||||||
private static int fillPingBlock(OutputStream ping,
|
|
||||||
Reader reader, String endPattern)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
int total = 0;
|
|
||||||
int endIndex = 0;
|
|
||||||
char [] block = new char[TRACES_BLOCK_SIZE];
|
|
||||||
for (int size = reader.read(block); size >= 0; size = reader.read(block)) {
|
|
||||||
String stringBlock = new String(block, 0, size);
|
|
||||||
endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex);
|
|
||||||
if (endIndex > 0) {
|
|
||||||
// Found end pattern; clip the string
|
|
||||||
stringBlock = stringBlock.substring(0, endIndex);
|
|
||||||
}
|
|
||||||
String quoted = JSONObject.quote(stringBlock);
|
|
||||||
total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1));
|
|
||||||
if (endIndex > 0) {
|
|
||||||
// End pattern already found; return now
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void fillLogcat(final OutputStream ping) {
|
|
||||||
if (Versions.preJB) {
|
|
||||||
// Logcat retrieval is not supported on pre-JB devices.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// get the last 200 lines of logcat
|
|
||||||
Process proc = (new ProcessBuilder())
|
|
||||||
.command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start();
|
|
||||||
try {
|
|
||||||
Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET);
|
|
||||||
int size = fillPingBlock(ping, procOut, null);
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
proc.destroy();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// ignore because logcat is not essential
|
|
||||||
Log.w(LOGTAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void fillPingFooter(OutputStream ping,
|
|
||||||
boolean haveNativeStack)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
// We are at the end of ANR data
|
|
||||||
|
|
||||||
int total = writePingPayload(ping, ("\"," +
|
|
||||||
"\"androidLogcat\":\""));
|
|
||||||
fillLogcat(ping);
|
|
||||||
|
|
||||||
if (haveNativeStack) {
|
|
||||||
total += writePingPayload(ping, ("\"," +
|
|
||||||
"\"androidNativeStack\":"));
|
|
||||||
|
|
||||||
String nativeStack = String.valueOf(getNativeStack());
|
|
||||||
int size = writePingPayload(ping, nativeStack);
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size));
|
|
||||||
}
|
|
||||||
total += size + writePingPayload(ping, "}");
|
|
||||||
} else {
|
|
||||||
total += writePingPayload(ping, "\"}");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte [] data = (
|
|
||||||
"}").getBytes(PING_CHARSET);
|
|
||||||
ping.write(data);
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void processTraces(Reader traces, File pingFile) {
|
|
||||||
|
|
||||||
// Only get native stack if Gecko is running.
|
|
||||||
// Also, unwinding is memory intensive, so only unwind if we have enough memory.
|
|
||||||
final boolean haveNativeStack =
|
|
||||||
GeckoThread.isRunning() ?
|
|
||||||
requestNativeStack(/* unwind */ SysInfo.getMemSize() >= 640) : false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
OutputStream ping = new BufferedOutputStream(
|
|
||||||
new FileOutputStream(pingFile), TRACES_BLOCK_SIZE);
|
|
||||||
try {
|
|
||||||
fillPingHeader(ping, pingFile.getName());
|
|
||||||
// Traces file has the format
|
|
||||||
// ----- pid xxx at xxx -----
|
|
||||||
// Cmd line: org.mozilla.xxx
|
|
||||||
// * stack trace *
|
|
||||||
// ----- end xxx -----
|
|
||||||
// ----- pid xxx at xxx -----
|
|
||||||
// Cmd line: com.android.xxx
|
|
||||||
// * stack trace *
|
|
||||||
// ...
|
|
||||||
// If we end the stack dump at the first end marker,
|
|
||||||
// only Fennec stacks will be dumped
|
|
||||||
int size = fillPingBlock(ping, traces, "\n----- end");
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size));
|
|
||||||
}
|
|
||||||
fillPingFooter(ping, haveNativeStack);
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "finished creating ping file");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
ping.close();
|
|
||||||
if (haveNativeStack) {
|
|
||||||
releaseNativeStack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(LOGTAG, e);
|
|
||||||
}
|
|
||||||
// exception; delete ping file
|
|
||||||
if (pingFile.exists()) {
|
|
||||||
pingFile.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void processTraces(File tracesFile, File pingFile) {
|
|
||||||
try {
|
|
||||||
Reader traces = new InputStreamReader(
|
|
||||||
new FileInputStream(tracesFile), TRACES_CHARSET);
|
|
||||||
try {
|
|
||||||
processTraces(traces, pingFile);
|
|
||||||
} finally {
|
|
||||||
traces.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(LOGTAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (mPendingANR) {
|
|
||||||
// we already processed an ANR without getting unstuck; skip this one
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "skipping duplicate ANR");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ThreadUtils.getUiHandler() != null) {
|
|
||||||
mPendingANR = true;
|
|
||||||
// detect when the main thread gets unstuck
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// okay to reset mPendingANR on main thread
|
|
||||||
mPendingANR = false;
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "yay we got unstuck!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "receiving " + String.valueOf(intent));
|
|
||||||
}
|
|
||||||
if (!ANR_ACTION.equals(intent.getAction())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure we have a good save location first
|
|
||||||
File pingFile = getPingFile();
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile));
|
|
||||||
}
|
|
||||||
if (pingFile == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
File tracesFile = getTracesFile();
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile));
|
|
||||||
}
|
|
||||||
if (tracesFile == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We get ANR intents from all ANRs in the system, but we only want Gecko ANRs
|
|
||||||
if (!isGeckoTraces(context.getPackageName(), tracesFile)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "traces is not Gecko ANR");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Log.i(LOGTAG, "processing Gecko ANR");
|
|
||||||
processTraces(tracesFile, pingFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.annotation.RobocopTarget;
|
|
||||||
import org.mozilla.gecko.home.HomeConfig;
|
|
||||||
import org.mozilla.gecko.home.HomeConfig.PanelType;
|
|
||||||
import org.mozilla.gecko.util.StringUtils;
|
|
||||||
|
|
||||||
public class AboutPages {
|
|
||||||
// All of our special pages.
|
|
||||||
public static final String ACCOUNTS = "about:accounts";
|
|
||||||
public static final String ADDONS = "about:addons";
|
|
||||||
public static final String CONFIG = "about:config";
|
|
||||||
public static final String DOWNLOADS = "about:downloads";
|
|
||||||
public static final String FIREFOX = "about:firefox";
|
|
||||||
public static final String HEALTHREPORT = "about:healthreport";
|
|
||||||
public static final String HOME = "about:home";
|
|
||||||
public static final String LOGINS = "about:logins";
|
|
||||||
public static final String PRIVATEBROWSING = "about:privatebrowsing";
|
|
||||||
public static final String READER = "about:reader";
|
|
||||||
public static final String UPDATER = "about:";
|
|
||||||
|
|
||||||
public static final String URL_FILTER = "about:%";
|
|
||||||
|
|
||||||
public static final String PANEL_PARAM = "panel";
|
|
||||||
|
|
||||||
public static final boolean isAboutPage(final String url) {
|
|
||||||
return url != null && url.startsWith("about:");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final boolean isTitlelessAboutPage(final String url) {
|
|
||||||
return isAboutHome(url) ||
|
|
||||||
PRIVATEBROWSING.equals(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final boolean isAboutHome(final String url) {
|
|
||||||
if (url == null || !url.startsWith(HOME)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// We sometimes append a parameter to "about:home" to specify which page to
|
|
||||||
// show when we open the home pager. Discard this parameter when checking
|
|
||||||
// whether or not this URL is "about:home".
|
|
||||||
return HOME.equals(url.split("\\?")[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String getPanelIdFromAboutHomeUrl(String aboutHomeUrl) {
|
|
||||||
return StringUtils.getQueryParameter(aboutHomeUrl, PANEL_PARAM);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isAboutReader(final String url) {
|
|
||||||
return isAboutPage(READER, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isAboutConfig(final String url) {
|
|
||||||
return isAboutPage(CONFIG, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isAboutAddons(final String url) {
|
|
||||||
return isAboutPage(ADDONS, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isAboutPrivateBrowsing(final String url) {
|
|
||||||
return isAboutPage(PRIVATEBROWSING, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isAboutPage(String page, String url) {
|
|
||||||
return url != null && url.toLowerCase().startsWith(page);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String[] DEFAULT_ICON_PAGES = new String[] {
|
|
||||||
HOME,
|
|
||||||
ACCOUNTS,
|
|
||||||
ADDONS,
|
|
||||||
CONFIG,
|
|
||||||
DOWNLOADS,
|
|
||||||
FIREFOX,
|
|
||||||
HEALTHREPORT,
|
|
||||||
UPDATER
|
|
||||||
};
|
|
||||||
|
|
||||||
public static boolean isBuiltinIconPage(final String url) {
|
|
||||||
if (url == null ||
|
|
||||||
!url.startsWith("about:")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// about:home uses a separate search built-in icon.
|
|
||||||
if (isAboutHome(url)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: it'd be quicker to not compare the "about:" part every time.
|
|
||||||
for (int i = 0; i < DEFAULT_ICON_PAGES.length; ++i) {
|
|
||||||
if (DEFAULT_ICON_PAGES[i].equals(url)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a URL that navigates to the specified built-in Home Panel.
|
|
||||||
*
|
|
||||||
* @param panelType to navigate to.
|
|
||||||
* @return URL.
|
|
||||||
* @throws IllegalArgumentException if the built-in panel type is not a built-in panel.
|
|
||||||
*/
|
|
||||||
@RobocopTarget
|
|
||||||
public static String getURLForBuiltinPanelType(PanelType panelType) throws IllegalArgumentException {
|
|
||||||
return HOME + "?panel=" + HomeConfig.getIdForBuiltinPanelType(panelType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,318 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AccountManagerCallback;
|
|
||||||
import android.accounts.AccountManagerFuture;
|
|
||||||
import android.accounts.AuthenticatorException;
|
|
||||||
import android.accounts.OperationCanceledException;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.background.fxa.FxAccountUtils;
|
|
||||||
import org.mozilla.gecko.fxa.FirefoxAccounts;
|
|
||||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
|
||||||
import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator;
|
|
||||||
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
|
|
||||||
import org.mozilla.gecko.fxa.login.Engaged;
|
|
||||||
import org.mozilla.gecko.fxa.login.State;
|
|
||||||
import org.mozilla.gecko.restrictions.Restrictable;
|
|
||||||
import org.mozilla.gecko.restrictions.Restrictions;
|
|
||||||
import org.mozilla.gecko.sync.SyncConfiguration;
|
|
||||||
import org.mozilla.gecko.sync.Utils;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
import org.mozilla.gecko.util.NativeEventListener;
|
|
||||||
import org.mozilla.gecko.util.NativeJSObject;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class to manage Android Accounts corresponding to Firefox Accounts.
|
|
||||||
*/
|
|
||||||
public class AccountsHelper implements NativeEventListener {
|
|
||||||
public static final String LOGTAG = "GeckoAccounts";
|
|
||||||
|
|
||||||
protected final Context mContext;
|
|
||||||
protected final GeckoProfile mProfile;
|
|
||||||
|
|
||||||
public AccountsHelper(Context context, GeckoProfile profile) {
|
|
||||||
mContext = context;
|
|
||||||
mProfile = profile;
|
|
||||||
|
|
||||||
EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
|
|
||||||
if (dispatcher == null) {
|
|
||||||
Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatcher.registerGeckoThreadListener(this,
|
|
||||||
"Accounts:CreateFirefoxAccountFromJSON",
|
|
||||||
"Accounts:UpdateFirefoxAccountFromJSON",
|
|
||||||
"Accounts:Create",
|
|
||||||
"Accounts:DeleteFirefoxAccount",
|
|
||||||
"Accounts:Exist",
|
|
||||||
"Accounts:ProfileUpdated",
|
|
||||||
"Accounts:ShowSyncPreferences");
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void uninit() {
|
|
||||||
EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
|
|
||||||
if (dispatcher == null) {
|
|
||||||
Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatcher.unregisterGeckoThreadListener(this,
|
|
||||||
"Accounts:CreateFirefoxAccountFromJSON",
|
|
||||||
"Accounts:UpdateFirefoxAccountFromJSON",
|
|
||||||
"Accounts:Create",
|
|
||||||
"Accounts:DeleteFirefoxAccount",
|
|
||||||
"Accounts:Exist",
|
|
||||||
"Accounts:ProfileUpdated",
|
|
||||||
"Accounts:ShowSyncPreferences");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(String event, NativeJSObject message, final EventCallback callback) {
|
|
||||||
if (!Restrictions.isAllowed(mContext, Restrictable.MODIFY_ACCOUNTS)) {
|
|
||||||
// We register for messages in all contexts; we drop, with a log and an error to JavaScript,
|
|
||||||
// when the profile is restricted. It's better to return errors than silently ignore messages.
|
|
||||||
Log.e(LOGTAG, "Profile is not allowed to modify accounts! Ignoring event: " + event);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError("Profile is not allowed to modify accounts!");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) {
|
|
||||||
// As we are about to create a new account, let's ensure our in-memory accounts cache
|
|
||||||
// is empty so that there are no undesired side-effects.
|
|
||||||
AndroidFxAccount.invalidateCaches();
|
|
||||||
|
|
||||||
AndroidFxAccount fxAccount = null;
|
|
||||||
try {
|
|
||||||
final NativeJSObject json = message.getObject("json");
|
|
||||||
final String email = json.getString("email");
|
|
||||||
final String uid = json.getString("uid");
|
|
||||||
final boolean verified = json.optBoolean("verified", false);
|
|
||||||
final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
|
|
||||||
final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
|
|
||||||
final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
|
|
||||||
final String authServerEndpoint =
|
|
||||||
json.optString("authServerEndpoint", FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT);
|
|
||||||
final String tokenServerEndpoint =
|
|
||||||
json.optString("tokenServerEndpoint", FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT);
|
|
||||||
final String profileServerEndpoint =
|
|
||||||
json.optString("profileServerEndpoint", FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
|
|
||||||
// TODO: handle choose what to Sync.
|
|
||||||
State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
|
|
||||||
fxAccount = AndroidFxAccount.addAndroidAccount(mContext,
|
|
||||||
email,
|
|
||||||
mProfile.getName(),
|
|
||||||
authServerEndpoint,
|
|
||||||
tokenServerEndpoint,
|
|
||||||
profileServerEndpoint,
|
|
||||||
state,
|
|
||||||
AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
|
|
||||||
|
|
||||||
final String[] declinedSyncEngines = json.optStringArray("declinedSyncEngines", null);
|
|
||||||
if (declinedSyncEngines != null) {
|
|
||||||
Log.i(LOGTAG, "User has selected engines; storing to prefs.");
|
|
||||||
final Map<String, Boolean> selectedEngines = new HashMap<String, Boolean>();
|
|
||||||
for (String enabledSyncEngine : SyncConfiguration.validEngineNames()) {
|
|
||||||
selectedEngines.put(enabledSyncEngine, true);
|
|
||||||
}
|
|
||||||
for (String declinedSyncEngine : declinedSyncEngines) {
|
|
||||||
selectedEngines.put(declinedSyncEngine, false);
|
|
||||||
}
|
|
||||||
// The "forms" engine has the same state as the "history" engine.
|
|
||||||
selectedEngines.put("forms", selectedEngines.get("history"));
|
|
||||||
FxAccountUtils.pii(LOGTAG, "User selected engines: " + selectedEngines.toString());
|
|
||||||
try {
|
|
||||||
SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines);
|
|
||||||
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
|
|
||||||
Log.e(LOGTAG, "Got exception storing selected engines; ignoring.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (URISyntaxException | GeneralSecurityException | UnsupportedEncodingException e) {
|
|
||||||
Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError("Could not create Firefox Account from JSON: " + e.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendSuccess(fxAccount != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if ("Accounts:UpdateFirefoxAccountFromJSON".equals(event)) {
|
|
||||||
// We might be significantly changing state of the account; let's ensure our in-memory
|
|
||||||
// accounts cache is empty so that there are no undesired side-effects.
|
|
||||||
AndroidFxAccount.invalidateCaches();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
|
|
||||||
if (account == null) {
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError("Could not update Firefox Account since none exists");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final NativeJSObject json = message.getObject("json");
|
|
||||||
final String email = json.getString("email");
|
|
||||||
final String uid = json.getString("uid");
|
|
||||||
|
|
||||||
// Protect against cross-connecting accounts.
|
|
||||||
if (account.name == null || !account.name.equals(email)) {
|
|
||||||
final String errorMessage = "Cannot update Firefox Account from JSON: datum has different email address!";
|
|
||||||
Log.e(LOGTAG, errorMessage);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError(errorMessage);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final boolean verified = json.optBoolean("verified", false);
|
|
||||||
final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
|
|
||||||
final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
|
|
||||||
final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
|
|
||||||
final State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
|
|
||||||
|
|
||||||
final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
|
|
||||||
fxAccount.setState(state);
|
|
||||||
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendSuccess(true);
|
|
||||||
}
|
|
||||||
} catch (NativeJSObject.InvalidPropertyException e) {
|
|
||||||
Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError("Could not update Firefox Account from JSON: " + e.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if ("Accounts:Create".equals(event)) {
|
|
||||||
// Do exactly the same thing as if you tapped 'Sync' in Settings.
|
|
||||||
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
final NativeJSObject extras = message.optObject("extras", null);
|
|
||||||
if (extras != null) {
|
|
||||||
intent.putExtra("extras", extras.toString());
|
|
||||||
}
|
|
||||||
mContext.startActivity(intent);
|
|
||||||
|
|
||||||
} else if ("Accounts:DeleteFirefoxAccount".equals(event)) {
|
|
||||||
try {
|
|
||||||
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
|
|
||||||
if (account == null) {
|
|
||||||
Log.w(LOGTAG, "Could not delete Firefox Account since none exists!");
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError("Could not delete Firefox Account since none exists");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final AccountManagerCallback<Boolean> accountManagerCallback = new AccountManagerCallback<Boolean>() {
|
|
||||||
@Override
|
|
||||||
public void run(AccountManagerFuture<Boolean> future) {
|
|
||||||
try {
|
|
||||||
final boolean result = future.getResult();
|
|
||||||
Log.i(LOGTAG, "Account named like " + Utils.obfuscateEmail(account.name) + " removed: " + result);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendSuccess(result);
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError("Could not delete Firefox Account: " + e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
AccountManager.get(mContext).removeAccount(account, accountManagerCallback, null);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError("Could not update Firefox Account from JSON: " + e.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if ("Accounts:Exist".equals(event)) {
|
|
||||||
if (callback == null) {
|
|
||||||
Log.w(LOGTAG, "Accounts:Exist requires a callback");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String kind = message.optString("kind", null);
|
|
||||||
final JSONObject response = new JSONObject();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ("any".equals(kind)) {
|
|
||||||
response.put("exists", FirefoxAccounts.firefoxAccountsExist(mContext));
|
|
||||||
callback.sendSuccess(response);
|
|
||||||
} else if ("fxa".equals(kind)) {
|
|
||||||
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
|
|
||||||
response.put("exists", account != null);
|
|
||||||
if (account != null) {
|
|
||||||
response.put("email", account.name);
|
|
||||||
// We should always be able to extract the server endpoints.
|
|
||||||
final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
|
|
||||||
response.put("authServerEndpoint", fxAccount.getAccountServerURI());
|
|
||||||
response.put("profileServerEndpoint", fxAccount.getProfileServerURI());
|
|
||||||
response.put("tokenServerEndpoint", fxAccount.getTokenServerURI());
|
|
||||||
try {
|
|
||||||
// It is possible for the state fetch to fail and us to not be able to provide a UID.
|
|
||||||
// Long term, the UID (and verification flag) will be attached to the Android account
|
|
||||||
// user data and not the internal state representation.
|
|
||||||
final State state = fxAccount.getState();
|
|
||||||
response.put("uid", state.uid);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w(LOGTAG, "Got exception extracting account UID; ignoring.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.sendSuccess(response);
|
|
||||||
} else {
|
|
||||||
callback.sendError("Could not query account existence: unknown kind.");
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.w(LOGTAG, "Got exception querying account existence; ignoring.", e);
|
|
||||||
callback.sendError("Could not query account existence: " + e.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if ("Accounts:ProfileUpdated".equals(event)) {
|
|
||||||
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
|
|
||||||
if (account == null) {
|
|
||||||
Log.w(LOGTAG, "Can't change profile of non-existent Firefox Account!; ignored");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final AndroidFxAccount androidFxAccount = new AndroidFxAccount(mContext, account);
|
|
||||||
androidFxAccount.fetchProfileJSON();
|
|
||||||
} else if ("Accounts:ShowSyncPreferences".equals(event)) {
|
|
||||||
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
|
|
||||||
if (account == null) {
|
|
||||||
Log.w(LOGTAG, "Can't change show Sync preferences of non-existent Firefox Account!; ignored");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We don't necessarily have an Activity context here, so we always start in a new task.
|
|
||||||
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
mContext.startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,256 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.menu.GeckoMenu;
|
|
||||||
import org.mozilla.gecko.menu.GeckoMenuItem;
|
|
||||||
import org.mozilla.gecko.util.ResourceDrawableUtils;
|
|
||||||
import org.mozilla.gecko.text.TextSelection;
|
|
||||||
import org.mozilla.gecko.util.GeckoEventListener;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
import org.mozilla.gecko.ActionModeCompat.Callback;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
class ActionBarTextSelection implements TextSelection, GeckoEventListener {
|
|
||||||
private static final String LOGTAG = "GeckoTextSelection";
|
|
||||||
private static final int SHUTDOWN_DELAY_MS = 250;
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
private boolean mDraggingHandles;
|
|
||||||
|
|
||||||
private String selectionID; // Unique ID provided for each selection action.
|
|
||||||
|
|
||||||
private String mCurrentItems;
|
|
||||||
|
|
||||||
private TextSelectionActionModeCallback mCallback;
|
|
||||||
|
|
||||||
// These timers are used to avoid flicker caused by selection handles showing/hiding quickly.
|
|
||||||
// For instance when moving between single handle caret mode and two handle selection mode.
|
|
||||||
private final Timer mActionModeTimer = new Timer("actionMode");
|
|
||||||
private class ActionModeTimerTask extends TimerTask {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
endActionMode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private ActionModeTimerTask mActionModeTimerTask;
|
|
||||||
|
|
||||||
ActionBarTextSelection(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void create() {
|
|
||||||
// Only register listeners if we have valid start/middle/end handles
|
|
||||||
if (context == null) {
|
|
||||||
Log.e(LOGTAG, "Failed to initialize text selection because at least one context is null");
|
|
||||||
} else {
|
|
||||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
|
|
||||||
"TextSelection:ActionbarInit",
|
|
||||||
"TextSelection:ActionbarStatus",
|
|
||||||
"TextSelection:ActionbarUninit",
|
|
||||||
"TextSelection:Update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dismiss() {
|
|
||||||
// We do not call endActionMode() here because this is already handled by the activity.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() {
|
|
||||||
if (context == null) {
|
|
||||||
Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since context is null");
|
|
||||||
} else {
|
|
||||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
|
|
||||||
"TextSelection:ActionbarInit",
|
|
||||||
"TextSelection:ActionbarStatus",
|
|
||||||
"TextSelection:ActionbarUninit",
|
|
||||||
"TextSelection:Update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(final String event, final JSONObject message) {
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
if (event.equals("TextSelection:Update")) {
|
|
||||||
if (mActionModeTimerTask != null)
|
|
||||||
mActionModeTimerTask.cancel();
|
|
||||||
showActionMode(message.getJSONArray("actions"));
|
|
||||||
} else if (event.equals("TextSelection:ActionbarInit")) {
|
|
||||||
// Init / Open the action bar. Note the current selectionID,
|
|
||||||
// cancel any pending actionBar close.
|
|
||||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
|
|
||||||
TelemetryContract.Method.CONTENT, "text_selection");
|
|
||||||
|
|
||||||
selectionID = message.getString("selectionID");
|
|
||||||
mCurrentItems = null;
|
|
||||||
if (mActionModeTimerTask != null) {
|
|
||||||
mActionModeTimerTask.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (event.equals("TextSelection:ActionbarStatus")) {
|
|
||||||
// Ensure async updates from SearchService for example are valid.
|
|
||||||
if (selectionID != message.optString("selectionID")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the actionBar actions as provided by Gecko.
|
|
||||||
showActionMode(message.getJSONArray("actions"));
|
|
||||||
|
|
||||||
} else if (event.equals("TextSelection:ActionbarUninit")) {
|
|
||||||
// Uninit the actionbar. Schedule a cancellable close
|
|
||||||
// action to avoid UI jank. (During SelectionAll for ex).
|
|
||||||
mCurrentItems = null;
|
|
||||||
mActionModeTimerTask = new ActionModeTimerTask();
|
|
||||||
mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(LOGTAG, "JSON exception", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showActionMode(final JSONArray items) {
|
|
||||||
String itemsString = items.toString();
|
|
||||||
if (itemsString.equals(mCurrentItems)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mCurrentItems = itemsString;
|
|
||||||
|
|
||||||
if (mCallback != null) {
|
|
||||||
mCallback.updateItems(items);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context instanceof ActionModeCompat.Presenter) {
|
|
||||||
final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
|
|
||||||
mCallback = new TextSelectionActionModeCallback(items);
|
|
||||||
presenter.startActionModeCompat(mCallback);
|
|
||||||
mCallback.animateIn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void endActionMode() {
|
|
||||||
if (context instanceof ActionModeCompat.Presenter) {
|
|
||||||
final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
|
|
||||||
presenter.endActionModeCompat();
|
|
||||||
}
|
|
||||||
mCurrentItems = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TextSelectionActionModeCallback implements Callback {
|
|
||||||
private JSONArray mItems;
|
|
||||||
private ActionModeCompat mActionMode;
|
|
||||||
|
|
||||||
public TextSelectionActionModeCallback(JSONArray items) {
|
|
||||||
mItems = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateItems(JSONArray items) {
|
|
||||||
mItems = items;
|
|
||||||
if (mActionMode != null) {
|
|
||||||
mActionMode.invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void animateIn() {
|
|
||||||
if (mActionMode != null) {
|
|
||||||
mActionMode.animateIn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPrepareActionMode(final ActionModeCompat mode, final GeckoMenu menu) {
|
|
||||||
// Android would normally expect us to only update the state of menu items here
|
|
||||||
// To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all
|
|
||||||
// the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the
|
|
||||||
// action mode.
|
|
||||||
menu.clear();
|
|
||||||
|
|
||||||
int length = mItems.length();
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
try {
|
|
||||||
final JSONObject obj = mItems.getJSONObject(i);
|
|
||||||
final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
|
|
||||||
final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER;
|
|
||||||
menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle);
|
|
||||||
|
|
||||||
final String iconString = obj.optString("icon");
|
|
||||||
ResourceDrawableUtils.getDrawable(context, iconString, new ResourceDrawableUtils.BitmapLoader() {
|
|
||||||
@Override
|
|
||||||
public void onBitmapFound(Drawable d) {
|
|
||||||
if (d != null) {
|
|
||||||
menuitem.setIcon(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Log.i(LOGTAG, "Exception building menu", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu unused) {
|
|
||||||
mActionMode = mode;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
|
|
||||||
try {
|
|
||||||
final JSONObject obj = mItems.getJSONObject(item.getItemId());
|
|
||||||
GeckoAppShell.notifyObservers("TextSelection:Action", obj.optString("id"));
|
|
||||||
return true;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Log.i(LOGTAG, "Exception calling action", ex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when the user exits the action mode
|
|
||||||
@Override
|
|
||||||
public void onDestroyActionMode(ActionModeCompat mode) {
|
|
||||||
mActionMode = null;
|
|
||||||
mCallback = null;
|
|
||||||
final JSONObject args = new JSONObject();
|
|
||||||
try {
|
|
||||||
args.put("selectionID", selectionID);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GeckoAppShell.notifyObservers("TextSelection:End", args.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.menu.GeckoMenu;
|
|
||||||
import org.mozilla.gecko.menu.GeckoMenuItem;
|
|
||||||
import org.mozilla.gecko.widget.GeckoPopupMenu;
|
|
||||||
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
class ActionModeCompat implements GeckoPopupMenu.OnMenuItemClickListener,
|
|
||||||
GeckoPopupMenu.OnMenuItemLongClickListener,
|
|
||||||
View.OnClickListener {
|
|
||||||
private final String LOGTAG = "GeckoActionModeCompat";
|
|
||||||
|
|
||||||
private final Callback mCallback;
|
|
||||||
private final ActionModeCompatView mView;
|
|
||||||
private final Presenter mPresenter;
|
|
||||||
|
|
||||||
/* A set of callbacks to be called during this ActionMode's lifecycle. These will control the
|
|
||||||
* creation, interaction with, and destruction of menuitems for the view */
|
|
||||||
public static interface Callback {
|
|
||||||
/* Called when action mode is first created. Implementors should use this to inflate menu resources. */
|
|
||||||
public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu menu);
|
|
||||||
|
|
||||||
/* Called to refresh an action mode's action menu. Called whenever the mode is invalidated. Implementors
|
|
||||||
* should use this to enable/disable/show/hide menu items. */
|
|
||||||
public boolean onPrepareActionMode(ActionModeCompat mode, GeckoMenu menu);
|
|
||||||
|
|
||||||
/* Called to report a user click on an action button. */
|
|
||||||
public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item);
|
|
||||||
|
|
||||||
/* Called when an action mode is about to be exited and destroyed. */
|
|
||||||
public void onDestroyActionMode(ActionModeCompat mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Presenters handle the actual showing/hiding of the action mode UI in the app. Its their responsibility
|
|
||||||
* to create an action mode, and assign it Callbacks and ActionModeCompatView's. */
|
|
||||||
public static interface Presenter {
|
|
||||||
/* Called when an action mode should be shown */
|
|
||||||
public void startActionModeCompat(final Callback callback);
|
|
||||||
|
|
||||||
/* Called when whatever action mode is showing should be hidden */
|
|
||||||
public void endActionModeCompat();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ActionModeCompat(Presenter presenter, Callback callback, ActionModeCompatView view) {
|
|
||||||
mPresenter = presenter;
|
|
||||||
mCallback = callback;
|
|
||||||
|
|
||||||
mView = view;
|
|
||||||
mView.initForMode(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void finish() {
|
|
||||||
// Clearing the menu will also clear the ActionItemBar
|
|
||||||
final GeckoMenu menu = mView.getMenu();
|
|
||||||
menu.clear();
|
|
||||||
menu.close();
|
|
||||||
|
|
||||||
if (mCallback != null) {
|
|
||||||
mCallback.onDestroyActionMode(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSequence getTitle() {
|
|
||||||
return mView.getTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(CharSequence title) {
|
|
||||||
mView.setTitle(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(int resId) {
|
|
||||||
mView.setTitle(resId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GeckoMenu getMenu() {
|
|
||||||
return mView.getMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void invalidate() {
|
|
||||||
if (mCallback != null) {
|
|
||||||
mCallback.onPrepareActionMode(this, mView.getMenu());
|
|
||||||
}
|
|
||||||
mView.invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void animateIn() {
|
|
||||||
mView.animateIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GeckoPopupMenu.OnMenuItemClickListener */
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
|
||||||
if (mCallback != null) {
|
|
||||||
return mCallback.onActionItemClicked(this, item);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GeckoPopupMenu.onMenuItemLongClickListener */
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemLongClick(MenuItem item) {
|
|
||||||
showTooltip((GeckoMenuItem) item);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* View.OnClickListener*/
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
mPresenter.endActionModeCompat();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showTooltip(GeckoMenuItem item) {
|
|
||||||
// Computes the tooltip toast screen position (shown when long-tapping the menu item) with regards to the
|
|
||||||
// menu item's position (i.e below the item and slightly to the left)
|
|
||||||
int[] location = new int[2];
|
|
||||||
final View view = item.getActionView();
|
|
||||||
view.getLocationOnScreen(location);
|
|
||||||
|
|
||||||
int xOffset = location[0] - view.getWidth();
|
|
||||||
int yOffset = location[1] + view.getHeight() / 2;
|
|
||||||
|
|
||||||
Toast toast = Toast.makeText(view.getContext(), item.getTitle(), Toast.LENGTH_SHORT);
|
|
||||||
toast.setGravity(Gravity.TOP | Gravity.LEFT, xOffset, yOffset);
|
|
||||||
toast.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import org.mozilla.gecko.animation.AnimationUtils;
|
|
||||||
import org.mozilla.gecko.menu.GeckoMenu;
|
|
||||||
import org.mozilla.gecko.widget.GeckoPopupMenu;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.ScaleAnimation;
|
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
|
|
||||||
class ActionModeCompatView extends LinearLayout implements GeckoMenu.ActionItemBarPresenter {
|
|
||||||
private final String LOGTAG = "GeckoActionModeCompatPresenter";
|
|
||||||
|
|
||||||
private static final int SPEC = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
|
|
||||||
private Button mTitleView;
|
|
||||||
private ImageButton mMenuButton;
|
|
||||||
private ViewGroup mActionButtonBar;
|
|
||||||
private GeckoPopupMenu mPopupMenu;
|
|
||||||
|
|
||||||
// Maximum number of items to show as actions
|
|
||||||
private static final int MAX_ACTION_ITEMS = 4;
|
|
||||||
|
|
||||||
private int mActionButtonsWidth;
|
|
||||||
|
|
||||||
private Paint mBottomDividerPaint;
|
|
||||||
private int mBottomDividerOffset;
|
|
||||||
|
|
||||||
public ActionModeCompatView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init(context, attrs, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ActionModeCompatView(Context context, AttributeSet attrs, int style) {
|
|
||||||
super(context, attrs, style);
|
|
||||||
init(context, attrs, style);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init(final Context context, final AttributeSet attrs, final int defStyle) {
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.actionbar, this);
|
|
||||||
|
|
||||||
mTitleView = (Button) findViewById(R.id.actionmode_title);
|
|
||||||
mMenuButton = (ImageButton) findViewById(R.id.actionbar_menu);
|
|
||||||
mActionButtonBar = (ViewGroup) findViewById(R.id.actionbar_buttons);
|
|
||||||
|
|
||||||
mPopupMenu = new GeckoPopupMenu(getContext(), mMenuButton);
|
|
||||||
mPopupMenu.getMenu().setActionItemBarPresenter(this);
|
|
||||||
|
|
||||||
mMenuButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
openMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// The built-in action bar uses colorAccent for the divider so we duplicate that here.
|
|
||||||
final TypedArray arr = context.obtainStyledAttributes(attrs, new int[] { R.attr.colorAccent }, defStyle, 0);
|
|
||||||
final int bottomDividerColor = arr.getColor(0, 0);
|
|
||||||
arr.recycle();
|
|
||||||
|
|
||||||
mBottomDividerPaint = new Paint();
|
|
||||||
mBottomDividerPaint.setColor(bottomDividerColor);
|
|
||||||
mBottomDividerOffset = getResources().getDimensionPixelSize(R.dimen.action_bar_divider_height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initForMode(final ActionModeCompat mode) {
|
|
||||||
mTitleView.setOnClickListener(mode);
|
|
||||||
mPopupMenu.setOnMenuItemClickListener(mode);
|
|
||||||
mPopupMenu.setOnMenuItemLongClickListener(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSequence getTitle() {
|
|
||||||
return mTitleView.getText();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(CharSequence title) {
|
|
||||||
mTitleView.setText(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(int resId) {
|
|
||||||
mTitleView.setText(resId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GeckoMenu getMenu() {
|
|
||||||
return mPopupMenu.getMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void invalidate() {
|
|
||||||
// onFinishInflate may not have been called yet on some versions of Android
|
|
||||||
if (mPopupMenu != null && mMenuButton != null) {
|
|
||||||
mMenuButton.setVisibility(mPopupMenu.getMenu().hasVisibleItems() ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
super.invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GeckoMenu.ActionItemBarPresenter */
|
|
||||||
@Override
|
|
||||||
public boolean addActionItem(View actionItem) {
|
|
||||||
final int count = mActionButtonBar.getChildCount();
|
|
||||||
if (count >= MAX_ACTION_ITEMS) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int maxWidth = mActionButtonBar.getMeasuredWidth();
|
|
||||||
if (maxWidth == 0) {
|
|
||||||
mActionButtonBar.measure(SPEC, SPEC);
|
|
||||||
maxWidth = mActionButtonBar.getMeasuredWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the menu button is already visible, no need to account for it
|
|
||||||
if (mMenuButton.getVisibility() == View.GONE) {
|
|
||||||
// Since we don't know how many items will be added, we always reserve space for the overflow menu
|
|
||||||
mMenuButton.measure(SPEC, SPEC);
|
|
||||||
maxWidth -= mMenuButton.getMeasuredWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mActionButtonsWidth <= 0) {
|
|
||||||
mActionButtonsWidth = 0;
|
|
||||||
|
|
||||||
// Loop over child views, measure them, and add their width to the taken width
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
View v = mActionButtonBar.getChildAt(i);
|
|
||||||
v.measure(SPEC, SPEC);
|
|
||||||
mActionButtonsWidth += v.getMeasuredWidth();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actionItem.measure(SPEC, SPEC);
|
|
||||||
int w = actionItem.getMeasuredWidth();
|
|
||||||
if (mActionButtonsWidth + w < maxWidth) {
|
|
||||||
// We cache the new width of our children.
|
|
||||||
mActionButtonsWidth += w;
|
|
||||||
mActionButtonBar.addView(actionItem);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GeckoMenu.ActionItemBarPresenter */
|
|
||||||
@Override
|
|
||||||
public void removeActionItem(View actionItem) {
|
|
||||||
actionItem.measure(SPEC, SPEC);
|
|
||||||
mActionButtonsWidth -= actionItem.getMeasuredWidth();
|
|
||||||
mActionButtonBar.removeView(actionItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openMenu() {
|
|
||||||
mPopupMenu.openMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void closeMenu() {
|
|
||||||
mPopupMenu.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void animateIn() {
|
|
||||||
long duration = AnimationUtils.getShortDuration(getContext());
|
|
||||||
TranslateAnimation t = new TranslateAnimation(Animation.RELATIVE_TO_SELF, -0.5f, Animation.RELATIVE_TO_SELF, 0f,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f);
|
|
||||||
t.setDuration(duration);
|
|
||||||
|
|
||||||
ScaleAnimation s = new ScaleAnimation(1f, 1f, 0f, 1f,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
s.setDuration((long) (duration * 1.5f));
|
|
||||||
|
|
||||||
mTitleView.startAnimation(t);
|
|
||||||
mActionButtonBar.startAnimation(s);
|
|
||||||
|
|
||||||
if ((mMenuButton.getVisibility() == View.VISIBLE) &&
|
|
||||||
(mPopupMenu.getMenu().size() > 0)) {
|
|
||||||
mMenuButton.startAnimation(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDraw(Canvas canvas) {
|
|
||||||
super.onDraw(canvas);
|
|
||||||
|
|
||||||
// Draw the divider at the bottom of the screen. We could do this with a layer-list
|
|
||||||
// but then we'd have overdraw (http://stackoverflow.com/a/13509472).
|
|
||||||
final int bottom = getHeight();
|
|
||||||
final int top = bottom - mBottomDividerOffset;
|
|
||||||
canvas.drawRect(0, top, getWidth(), bottom, mBottomDividerPaint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.util.ActivityResultHandler;
|
|
||||||
import org.mozilla.gecko.util.ActivityResultHandlerMap;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class ActivityHandlerHelper {
|
|
||||||
private static final String LOGTAG = "GeckoActivityHandlerHelper";
|
|
||||||
private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
|
|
||||||
|
|
||||||
private static int makeRequestCode(ActivityResultHandler aHandler) {
|
|
||||||
return mActivityResultHandlerMap.put(aHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void startIntent(Intent intent, ActivityResultHandler activityResultHandler) {
|
|
||||||
startIntentForActivity(GeckoAppShell.getGeckoInterface().getActivity(), intent, activityResultHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the Activity, catching & logging if the Activity fails to start.
|
|
||||||
*
|
|
||||||
* We catch to prevent callers from passing in invalid Intents and crashing the browser.
|
|
||||||
*
|
|
||||||
* @return true if the Activity is successfully started, false otherwise.
|
|
||||||
*/
|
|
||||||
public static boolean startIntentAndCatch(final String logtag, final Context context, final Intent intent) {
|
|
||||||
try {
|
|
||||||
context.startActivity(intent);
|
|
||||||
return true;
|
|
||||||
} catch (final ActivityNotFoundException e) {
|
|
||||||
Log.w(logtag, "Activity not found.", e);
|
|
||||||
return false;
|
|
||||||
} catch (final SecurityException e) {
|
|
||||||
Log.w(logtag, "Forbidden to launch activity.", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
|
|
||||||
activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
|
|
||||||
if (handler != null) {
|
|
||||||
handler.onActivityResult(resultCode, data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.feeds.FeedService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This broadcast receiver receives ACTION_BOOT_COMPLETED broadcasts and starts components that should
|
|
||||||
* run after the device has booted.
|
|
||||||
*/
|
|
||||||
public class BootReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (intent == null || !intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
|
|
||||||
return; // This is not the broadcast you are looking for.
|
|
||||||
}
|
|
||||||
|
|
||||||
FeedService.setup(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,439 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.annotation.ReflectionTarget;
|
|
||||||
import org.mozilla.gecko.util.GeckoJarReader;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class manages persistence, application, and otherwise handling of
|
|
||||||
* user-specified locales.
|
|
||||||
*
|
|
||||||
* Of note:
|
|
||||||
*
|
|
||||||
* * It's a singleton, because its scope extends to that of the application,
|
|
||||||
* and definitionally all changes to the locale of the app must go through
|
|
||||||
* this.
|
|
||||||
* * It's lazy.
|
|
||||||
* * It has ties into the Gecko event system, because it has to tell Gecko when
|
|
||||||
* to switch locale.
|
|
||||||
* * It relies on using the SharedPreferences file owned by the browser (in
|
|
||||||
* Fennec's case, "GeckoApp") for performance.
|
|
||||||
*/
|
|
||||||
public class BrowserLocaleManager implements LocaleManager {
|
|
||||||
private static final String LOG_TAG = "GeckoLocales";
|
|
||||||
|
|
||||||
private static final String EVENT_LOCALE_CHANGED = "Locale:Changed";
|
|
||||||
private static final String PREF_LOCALE = "locale";
|
|
||||||
|
|
||||||
private static final String FALLBACK_LOCALE_TAG = "en-US";
|
|
||||||
|
|
||||||
// These are volatile because we don't impose restrictions
|
|
||||||
// over which thread calls our methods.
|
|
||||||
private volatile Locale currentLocale;
|
|
||||||
private volatile Locale systemLocale = Locale.getDefault();
|
|
||||||
|
|
||||||
private final AtomicBoolean inited = new AtomicBoolean(false);
|
|
||||||
private boolean systemLocaleDidChange;
|
|
||||||
private BroadcastReceiver receiver;
|
|
||||||
|
|
||||||
private static final AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>();
|
|
||||||
|
|
||||||
@ReflectionTarget
|
|
||||||
public static LocaleManager getInstance() {
|
|
||||||
LocaleManager localeManager = instance.get();
|
|
||||||
if (localeManager != null) {
|
|
||||||
return localeManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
localeManager = new BrowserLocaleManager();
|
|
||||||
if (instance.compareAndSet(null, localeManager)) {
|
|
||||||
return localeManager;
|
|
||||||
} else {
|
|
||||||
return instance.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return AppConstants.MOZ_LOCALE_SWITCHER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure that you call this early in your application startup,
|
|
||||||
* and with a context that's sufficiently long-lived (typically
|
|
||||||
* the application context).
|
|
||||||
*
|
|
||||||
* Calling multiple times is harmless.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void initialize(final Context context) {
|
|
||||||
if (!inited.compareAndSet(false, true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
receiver = new BroadcastReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
final Locale current = systemLocale;
|
|
||||||
|
|
||||||
// We don't trust Locale.getDefault() here, because we make a
|
|
||||||
// habit of mutating it! Use the one Android supplies, because
|
|
||||||
// that gets regularly reset.
|
|
||||||
// The default value of systemLocale is fine, because we haven't
|
|
||||||
// yet swizzled Locale during static initialization.
|
|
||||||
systemLocale = context.getResources().getConfiguration().locale;
|
|
||||||
systemLocaleDidChange = true;
|
|
||||||
|
|
||||||
Log.d(LOG_TAG, "System locale changed from " + current + " to " + systemLocale);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean systemLocaleDidChange() {
|
|
||||||
return systemLocaleDidChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every time the system gives us a new configuration, it
|
|
||||||
* carries the external locale. Fix it.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void correctLocale(Context context, Resources res, Configuration config) {
|
|
||||||
final Locale current = getCurrentLocale(context);
|
|
||||||
if (current == null) {
|
|
||||||
Log.d(LOG_TAG, "No selected locale. No correction needed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// I know it's tempting to short-circuit here if the config seems to be
|
|
||||||
// up-to-date, but the rest is necessary.
|
|
||||||
|
|
||||||
config.locale = current;
|
|
||||||
|
|
||||||
// The following two lines are heavily commented in case someone
|
|
||||||
// decides to chase down performance improvements and decides to
|
|
||||||
// question what's going on here.
|
|
||||||
// Both lines should be cheap, *but*...
|
|
||||||
|
|
||||||
// This is unnecessary for basic string choice, but it almost
|
|
||||||
// certainly comes into play when rendering numbers, deciding on RTL,
|
|
||||||
// etc. Take it out if you can prove that's not the case.
|
|
||||||
Locale.setDefault(current);
|
|
||||||
|
|
||||||
// This seems to be a no-op, but every piece of documentation under the
|
|
||||||
// sun suggests that it's necessary, and it certainly makes sense.
|
|
||||||
res.updateConfiguration(config, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We can be in one of two states.
|
|
||||||
*
|
|
||||||
* If the user has not explicitly chosen a Firefox-specific locale, we say
|
|
||||||
* we are "mirroring" the system locale.
|
|
||||||
*
|
|
||||||
* When we are not mirroring, system locale changes do not impact Firefox
|
|
||||||
* and are essentially ignored; the user's locale selection is the only
|
|
||||||
* thing we care about, and we actively correct incoming configuration
|
|
||||||
* changes to reflect the user's chosen locale.
|
|
||||||
*
|
|
||||||
* By contrast, when we are mirroring, system locale changes cause Firefox
|
|
||||||
* to reflect the new system locale, as if the user picked the new locale.
|
|
||||||
*
|
|
||||||
* If we're currently mirroring the system locale, this method returns the
|
|
||||||
* supplied configuration's locale, unless the current activity locale is
|
|
||||||
* correct. If we're not currently mirroring, this method updates the
|
|
||||||
* configuration object to match the user's currently selected locale, and
|
|
||||||
* returns that, unless the current activity locale is correct.
|
|
||||||
*
|
|
||||||
* If the current activity locale is correct, returns null.
|
|
||||||
*
|
|
||||||
* The caller is expected to redisplay themselves accordingly.
|
|
||||||
*
|
|
||||||
* This method is intended to be called from inside
|
|
||||||
* <code>onConfigurationChanged(Configuration)</code> as part of a strategy
|
|
||||||
* to detect and either apply or undo system locale changes.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Locale onSystemConfigurationChanged(final Context context, final Resources resources, final Configuration configuration, final Locale currentActivityLocale) {
|
|
||||||
if (!isMirroringSystemLocale(context)) {
|
|
||||||
correctLocale(context, resources, configuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Locale changed = configuration.locale;
|
|
||||||
if (changed.equals(currentActivityLocale)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gecko needs to know the OS locale to compute a useful Accept-Language
|
|
||||||
* header. If it changed since last time, send a message to Gecko and
|
|
||||||
* persist the new value. If unchanged, returns immediately.
|
|
||||||
*
|
|
||||||
* @param prefs the SharedPreferences instance to use. Cannot be null.
|
|
||||||
* @param osLocale the new locale instance. Safe if null.
|
|
||||||
*/
|
|
||||||
public static void storeAndNotifyOSLocale(final SharedPreferences prefs,
|
|
||||||
final Locale osLocale) {
|
|
||||||
if (osLocale == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String lastOSLocale = prefs.getString("osLocale", null);
|
|
||||||
final String osLocaleString = osLocale.toString();
|
|
||||||
|
|
||||||
if (osLocaleString.equals(lastOSLocale)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the Java-native form.
|
|
||||||
prefs.edit().putString("osLocale", osLocaleString).apply();
|
|
||||||
|
|
||||||
// The value we send to Gecko should be a language tag, not
|
|
||||||
// a Java locale string.
|
|
||||||
final String osLanguageTag = Locales.getLanguageTag(osLocale);
|
|
||||||
GeckoAppShell.notifyObservers("Locale:OS", osLanguageTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAndApplyPersistedLocale(Context context) {
|
|
||||||
initialize(context);
|
|
||||||
|
|
||||||
final long t1 = android.os.SystemClock.uptimeMillis();
|
|
||||||
final String localeCode = getPersistedLocale(context);
|
|
||||||
if (localeCode == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that we don't tell Gecko about this. We notify Gecko when the
|
|
||||||
// locale is set, not when we update Java.
|
|
||||||
final String resultant = updateLocale(context, localeCode);
|
|
||||||
|
|
||||||
if (resultant == null) {
|
|
||||||
// Update the configuration anyway.
|
|
||||||
updateConfiguration(context, currentLocale);
|
|
||||||
}
|
|
||||||
|
|
||||||
final long t2 = android.os.SystemClock.uptimeMillis();
|
|
||||||
Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms.");
|
|
||||||
return resultant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the set locale if it changed.
|
|
||||||
*
|
|
||||||
* Always persists and notifies Gecko.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String setSelectedLocale(Context context, String localeCode) {
|
|
||||||
final String resultant = updateLocale(context, localeCode);
|
|
||||||
|
|
||||||
// We always persist and notify Gecko, even if nothing seemed to
|
|
||||||
// change. This might happen if you're picking a locale that's the same
|
|
||||||
// as the current OS locale. The OS locale might change next time we
|
|
||||||
// launch, and we need the Gecko pref and persisted locale to have been
|
|
||||||
// set by the time that happens.
|
|
||||||
persistLocale(context, localeCode);
|
|
||||||
|
|
||||||
// Tell Gecko.
|
|
||||||
GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context)));
|
|
||||||
|
|
||||||
return resultant;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void resetToSystemLocale(Context context) {
|
|
||||||
// Wipe the pref.
|
|
||||||
final SharedPreferences settings = getSharedPreferences(context);
|
|
||||||
settings.edit().remove(PREF_LOCALE).apply();
|
|
||||||
|
|
||||||
// Apply the system locale.
|
|
||||||
updateLocale(context, systemLocale);
|
|
||||||
|
|
||||||
// Tell Gecko.
|
|
||||||
GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is public to allow for an activity to force the
|
|
||||||
* current locale to be applied if necessary (e.g., when
|
|
||||||
* a new activity launches).
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void updateConfiguration(Context context, Locale locale) {
|
|
||||||
Resources res = context.getResources();
|
|
||||||
Configuration config = res.getConfiguration();
|
|
||||||
|
|
||||||
// We should use setLocale, but it's unexpectedly missing
|
|
||||||
// on real devices.
|
|
||||||
config.locale = locale;
|
|
||||||
res.updateConfiguration(config, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SharedPreferences getSharedPreferences(Context context) {
|
|
||||||
return GeckoSharedPrefs.forApp(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the persisted locale in Java format: "en_US".
|
|
||||||
*/
|
|
||||||
private String getPersistedLocale(Context context) {
|
|
||||||
final SharedPreferences settings = getSharedPreferences(context);
|
|
||||||
final String locale = settings.getString(PREF_LOCALE, "");
|
|
||||||
|
|
||||||
if ("".equals(locale)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void persistLocale(Context context, String localeCode) {
|
|
||||||
final SharedPreferences settings = getSharedPreferences(context);
|
|
||||||
settings.edit().putString(PREF_LOCALE, localeCode).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Locale getCurrentLocale(Context context) {
|
|
||||||
if (currentLocale != null) {
|
|
||||||
return currentLocale;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String current = getPersistedLocale(context);
|
|
||||||
if (current == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return currentLocale = Locales.parseLocaleCode(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the Java locale and the Android configuration.
|
|
||||||
*
|
|
||||||
* Returns the persisted locale if it differed.
|
|
||||||
*
|
|
||||||
* Does not notify Gecko.
|
|
||||||
*
|
|
||||||
* @param localeCode a locale string in Java format: "en_US".
|
|
||||||
* @return if it differed, a locale string in Java format: "en_US".
|
|
||||||
*/
|
|
||||||
private String updateLocale(Context context, String localeCode) {
|
|
||||||
// Fast path.
|
|
||||||
final Locale defaultLocale = Locale.getDefault();
|
|
||||||
if (defaultLocale.toString().equals(localeCode)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Locale locale = Locales.parseLocaleCode(localeCode);
|
|
||||||
|
|
||||||
return updateLocale(context, locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the Java locale string: e.g., "en_US".
|
|
||||||
*/
|
|
||||||
private String updateLocale(Context context, final Locale locale) {
|
|
||||||
// Fast path.
|
|
||||||
if (Locale.getDefault().equals(locale)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Locale.setDefault(locale);
|
|
||||||
currentLocale = locale;
|
|
||||||
|
|
||||||
// Update resources.
|
|
||||||
updateConfiguration(context, locale);
|
|
||||||
|
|
||||||
return locale.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isMirroringSystemLocale(final Context context) {
|
|
||||||
return getPersistedLocale(context) == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Examines <code>multilocale.json</code>, returning the included list of
|
|
||||||
* locale codes.
|
|
||||||
*
|
|
||||||
* If <code>multilocale.json</code> is not present, returns
|
|
||||||
* <code>null</code>. In that case, consider {@link #getFallbackLocaleTag()}.
|
|
||||||
*
|
|
||||||
* multilocale.json currently looks like this:
|
|
||||||
*
|
|
||||||
* <code>
|
|
||||||
* {"locales": ["en-US", "be", "ca", "cs", "da", "de", "en-GB",
|
|
||||||
* "en-ZA", "es-AR", "es-ES", "es-MX", "et", "fi",
|
|
||||||
* "fr", "ga-IE", "hu", "id", "it", "ja", "ko",
|
|
||||||
* "lt", "lv", "nb-NO", "nl", "pl", "pt-BR",
|
|
||||||
* "pt-PT", "ro", "ru", "sk", "sl", "sv-SE", "th",
|
|
||||||
* "tr", "uk", "zh-CN", "zh-TW", "en-US"]}
|
|
||||||
* </code>
|
|
||||||
*/
|
|
||||||
public static Collection<String> getPackagedLocaleTags(final Context context) {
|
|
||||||
final String resPath = "res/multilocale.json";
|
|
||||||
final String jarURL = GeckoJarReader.getJarURL(context, resPath);
|
|
||||||
|
|
||||||
final String contents = GeckoJarReader.getText(context, jarURL);
|
|
||||||
if (contents == null) {
|
|
||||||
// GeckoJarReader logs and swallows exceptions.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final JSONObject multilocale = new JSONObject(contents);
|
|
||||||
final JSONArray locales = multilocale.getJSONArray("locales");
|
|
||||||
if (locales == null) {
|
|
||||||
Log.e(LOG_TAG, "No 'locales' array in multilocales.json!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Set<String> out = new HashSet<String>(locales.length());
|
|
||||||
for (int i = 0; i < locales.length(); ++i) {
|
|
||||||
// If any item in the array is invalid, this will throw,
|
|
||||||
// and the entire clause will fail, being caught below
|
|
||||||
// and returning null.
|
|
||||||
out.add(locales.getString(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(LOG_TAG, "Unable to parse multilocale.json.", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the single default locale baked into this application.
|
|
||||||
* Applicable when there is no multilocale.json present.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("static-method")
|
|
||||||
public String getFallbackLocaleTag() {
|
|
||||||
return FALLBACK_LOCALE_TAG;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
||||||
* vim: ts=4 sw=4 expandtab:
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.json.JSONException;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.R;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
|
|
||||||
import com.google.android.gms.cast.CastDevice;
|
|
||||||
import com.google.android.gms.cast.CastRemoteDisplayLocalService;
|
|
||||||
import com.google.android.gms.common.ConnectionResult;
|
|
||||||
import com.google.android.gms.common.GooglePlayServicesUtil;
|
|
||||||
import com.google.android.gms.common.api.Status;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.support.v7.media.MediaRouter.RouteInfo;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class ChromeCastDisplay implements GeckoPresentationDisplay {
|
|
||||||
|
|
||||||
static final String REMOTE_DISPLAY_APP_ID = "4574A331";
|
|
||||||
|
|
||||||
private static final String LOGTAG = "GeckoChromeCastDisplay";
|
|
||||||
private final Context context;
|
|
||||||
private final RouteInfo route;
|
|
||||||
private CastDevice castDevice;
|
|
||||||
|
|
||||||
public ChromeCastDisplay(Context context, RouteInfo route) {
|
|
||||||
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
|
|
||||||
if (status != ConnectionResult.SUCCESS) {
|
|
||||||
throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context = context;
|
|
||||||
this.route = route;
|
|
||||||
this.castDevice = CastDevice.getFromBundle(route.getExtras());
|
|
||||||
}
|
|
||||||
|
|
||||||
public JSONObject toJSON() {
|
|
||||||
final JSONObject obj = new JSONObject();
|
|
||||||
try {
|
|
||||||
if (castDevice == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
obj.put("uuid", route.getId());
|
|
||||||
obj.put("friendlyName", castDevice.getFriendlyName());
|
|
||||||
obj.put("type", "chromecast");
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
Log.d(LOGTAG, "Error building route", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(final EventCallback callback) {
|
|
||||||
|
|
||||||
if (CastRemoteDisplayLocalService.getInstance() != null) {
|
|
||||||
Log.d(LOGTAG, "CastRemoteDisplayLocalService already existed.");
|
|
||||||
GeckoAppShell.notifyObservers("presentation-view-ready", route.getId());
|
|
||||||
callback.sendSuccess("Succeed to start presentation.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent(context, RemotePresentationService.class);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
||||||
PendingIntent notificationPendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
|
|
||||||
|
|
||||||
CastRemoteDisplayLocalService.NotificationSettings settings =
|
|
||||||
new CastRemoteDisplayLocalService.NotificationSettings.Builder()
|
|
||||||
.setNotificationPendingIntent(notificationPendingIntent).build();
|
|
||||||
|
|
||||||
CastRemoteDisplayLocalService.startService(
|
|
||||||
context,
|
|
||||||
RemotePresentationService.class,
|
|
||||||
REMOTE_DISPLAY_APP_ID,
|
|
||||||
castDevice,
|
|
||||||
settings,
|
|
||||||
new CastRemoteDisplayLocalService.Callbacks() {
|
|
||||||
@Override
|
|
||||||
public void onServiceCreated(CastRemoteDisplayLocalService service) {
|
|
||||||
((RemotePresentationService) service).setDeviceId(route.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRemoteDisplaySessionStarted(CastRemoteDisplayLocalService service) {
|
|
||||||
Log.d(LOGTAG, "Remote presentation launched!");
|
|
||||||
callback.sendSuccess("Succeed to start presentation.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRemoteDisplaySessionError(Status errorReason) {
|
|
||||||
int code = errorReason.getStatusCode();
|
|
||||||
callback.sendError("Fail to start presentation. Error code: " + code);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop(EventCallback callback) {
|
|
||||||
CastRemoteDisplayLocalService.stopService();
|
|
||||||
callback.sendSuccess("Succeed to stop presentation.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,509 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.json.JSONException;
|
|
||||||
|
|
||||||
import com.google.android.gms.cast.Cast.MessageReceivedCallback;
|
|
||||||
import com.google.android.gms.cast.ApplicationMetadata;
|
|
||||||
import com.google.android.gms.cast.Cast;
|
|
||||||
import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
|
|
||||||
import com.google.android.gms.cast.CastDevice;
|
|
||||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
|
||||||
import com.google.android.gms.cast.MediaMetadata;
|
|
||||||
import com.google.android.gms.cast.MediaStatus;
|
|
||||||
import com.google.android.gms.cast.RemoteMediaPlayer;
|
|
||||||
import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
|
|
||||||
import com.google.android.gms.common.ConnectionResult;
|
|
||||||
import com.google.android.gms.common.api.GoogleApiClient;
|
|
||||||
import com.google.android.gms.common.api.ResultCallback;
|
|
||||||
import com.google.android.gms.common.api.Status;
|
|
||||||
import com.google.android.gms.common.GooglePlayServicesUtil;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v7.media.MediaRouter.RouteInfo;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/* Implementation of GeckoMediaPlayer for talking to ChromeCast devices */
|
|
||||||
class ChromeCastPlayer implements GeckoMediaPlayer {
|
|
||||||
private static final boolean SHOW_DEBUG = false;
|
|
||||||
|
|
||||||
static final String MIRROR_RECEIVER_APP_ID = "08FF1091";
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final RouteInfo route;
|
|
||||||
private GoogleApiClient apiClient;
|
|
||||||
private RemoteMediaPlayer remoteMediaPlayer;
|
|
||||||
private final boolean canMirror;
|
|
||||||
private String mSessionId;
|
|
||||||
private MirrorChannel mMirrorChannel;
|
|
||||||
private boolean mApplicationStarted = false;
|
|
||||||
|
|
||||||
// EventCallback which is actually a GeckoEventCallback is sometimes being invoked more
|
|
||||||
// than once. That causes the IllegalStateException to be thrown. To prevent a crash,
|
|
||||||
// catch the exception and report it as an error to the log.
|
|
||||||
private static void sendSuccess(final EventCallback callback, final String msg) {
|
|
||||||
try {
|
|
||||||
callback.sendSuccess(msg);
|
|
||||||
} catch (final IllegalStateException e) {
|
|
||||||
Log.e(LOGTAG, "Attempting to invoke callback.sendSuccess more than once.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sendError(final EventCallback callback, final String msg) {
|
|
||||||
try {
|
|
||||||
callback.sendError(msg);
|
|
||||||
} catch (final IllegalStateException e) {
|
|
||||||
Log.e(LOGTAG, "Attempting to invoke callback.sendError more than once.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback to start playback of a url on a remote device
|
|
||||||
private class VideoPlayCallback implements ResultCallback<ApplicationConnectionResult>,
|
|
||||||
RemoteMediaPlayer.OnStatusUpdatedListener,
|
|
||||||
RemoteMediaPlayer.OnMetadataUpdatedListener {
|
|
||||||
private final String url;
|
|
||||||
private final String type;
|
|
||||||
private final String title;
|
|
||||||
private final EventCallback callback;
|
|
||||||
|
|
||||||
public VideoPlayCallback(String url, String type, String title, EventCallback callback) {
|
|
||||||
this.url = url;
|
|
||||||
this.type = type;
|
|
||||||
this.title = title;
|
|
||||||
this.callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStatusUpdated() {
|
|
||||||
MediaStatus mediaStatus = remoteMediaPlayer.getMediaStatus();
|
|
||||||
|
|
||||||
switch (mediaStatus.getPlayerState()) {
|
|
||||||
case MediaStatus.PLAYER_STATE_PLAYING:
|
|
||||||
GeckoAppShell.notifyObservers("MediaPlayer:Playing", null);
|
|
||||||
break;
|
|
||||||
case MediaStatus.PLAYER_STATE_PAUSED:
|
|
||||||
GeckoAppShell.notifyObservers("MediaPlayer:Paused", null);
|
|
||||||
break;
|
|
||||||
case MediaStatus.PLAYER_STATE_IDLE:
|
|
||||||
// TODO: Do we want to shutdown when there are errors?
|
|
||||||
if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
|
|
||||||
GeckoAppShell.notifyObservers("Casting:Stop", null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// TODO: Do we need to handle other status such as buffering / unknown?
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMetadataUpdated() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResult(ApplicationConnectionResult result) {
|
|
||||||
Status status = result.getStatus();
|
|
||||||
debug("ApplicationConnectionResultCallback.onResult: statusCode" + status.getStatusCode());
|
|
||||||
if (status.isSuccess()) {
|
|
||||||
remoteMediaPlayer = new RemoteMediaPlayer();
|
|
||||||
remoteMediaPlayer.setOnStatusUpdatedListener(this);
|
|
||||||
remoteMediaPlayer.setOnMetadataUpdatedListener(this);
|
|
||||||
mSessionId = result.getSessionId();
|
|
||||||
if (!verifySession(callback)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Cast.CastApi.setMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
|
|
||||||
} catch (IOException e) {
|
|
||||||
debug("Exception while creating media channel", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
startPlayback();
|
|
||||||
} else {
|
|
||||||
sendError(callback, status.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startPlayback() {
|
|
||||||
MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
|
||||||
mediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
|
|
||||||
MediaInfo mediaInfo = new MediaInfo.Builder(url)
|
|
||||||
.setContentType(type)
|
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
|
||||||
.setMetadata(mediaMetadata)
|
|
||||||
.build();
|
|
||||||
try {
|
|
||||||
remoteMediaPlayer.load(apiClient, mediaInfo, true).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
|
|
||||||
@Override
|
|
||||||
public void onResult(MediaChannelResult result) {
|
|
||||||
if (result.getStatus().isSuccess()) {
|
|
||||||
sendSuccess(callback, null);
|
|
||||||
debug("Media loaded successfully");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug("Media load failed " + result.getStatus());
|
|
||||||
sendError(callback, result.getStatus().toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
debug("Problem occurred with media during loading", e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
debug("Problem opening media during loading", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendError(callback, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChromeCastPlayer(Context context, RouteInfo route) {
|
|
||||||
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
|
|
||||||
if (status != ConnectionResult.SUCCESS) {
|
|
||||||
throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context = context;
|
|
||||||
this.route = route;
|
|
||||||
this.canMirror = route.supportsControlCategory(CastMediaControlIntent.categoryForCast(MIRROR_RECEIVER_APP_ID));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This dumps everything we can find about the device into JSON. This will hopefully make it
|
|
||||||
* easier to filter out duplicate devices from different sources in JS.
|
|
||||||
* Returns null if the device can't be found.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public JSONObject toJSON() {
|
|
||||||
final JSONObject obj = new JSONObject();
|
|
||||||
try {
|
|
||||||
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
|
|
||||||
if (device == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.put("uuid", route.getId());
|
|
||||||
obj.put("version", device.getDeviceVersion());
|
|
||||||
obj.put("friendlyName", device.getFriendlyName());
|
|
||||||
obj.put("location", device.getIpAddress().toString());
|
|
||||||
obj.put("modelName", device.getModelName());
|
|
||||||
obj.put("mirror", canMirror);
|
|
||||||
// For now we just assume all of these are Google devices
|
|
||||||
obj.put("manufacturer", "Google Inc.");
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
debug("Error building route", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void load(final String title, final String url, final String type, final EventCallback callback) {
|
|
||||||
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
|
|
||||||
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onApplicationStatusChanged() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onVolumeChanged() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onApplicationDisconnected(int errorCode) { }
|
|
||||||
});
|
|
||||||
|
|
||||||
apiClient = new GoogleApiClient.Builder(context)
|
|
||||||
.addApi(Cast.API, apiOptionsBuilder.build())
|
|
||||||
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
|
|
||||||
@Override
|
|
||||||
public void onConnected(Bundle connectionHint) {
|
|
||||||
// Sometimes apiClient is null here. See bug 1061032
|
|
||||||
if (apiClient != null && !apiClient.isConnected()) {
|
|
||||||
debug("Connection failed");
|
|
||||||
sendError(callback, "Not connected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch the media player app and launch this url once its loaded
|
|
||||||
try {
|
|
||||||
Cast.CastApi.launchApplication(apiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, true)
|
|
||||||
.setResultCallback(new VideoPlayCallback(url, type, title, callback));
|
|
||||||
} catch (Exception e) {
|
|
||||||
debug("Failed to launch application", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionSuspended(int cause) {
|
|
||||||
debug("suspended");
|
|
||||||
}
|
|
||||||
}).build();
|
|
||||||
|
|
||||||
apiClient.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(final EventCallback callback) {
|
|
||||||
// Nothing to be done here
|
|
||||||
sendSuccess(callback, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop(final EventCallback callback) {
|
|
||||||
// Nothing to be done here
|
|
||||||
sendSuccess(callback, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean verifySession(final EventCallback callback) {
|
|
||||||
String msg = null;
|
|
||||||
if (apiClient == null || !apiClient.isConnected()) {
|
|
||||||
msg = "Not connected";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mSessionId == null) {
|
|
||||||
msg = "No session";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg != null) {
|
|
||||||
debug(msg);
|
|
||||||
if (callback != null) {
|
|
||||||
sendError(callback, msg);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void play(final EventCallback callback) {
|
|
||||||
if (!verifySession(callback)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
|
|
||||||
@Override
|
|
||||||
public void onResult(MediaChannelResult result) {
|
|
||||||
Status status = result.getStatus();
|
|
||||||
if (!status.isSuccess()) {
|
|
||||||
debug("Unable to play: " + status.getStatusCode());
|
|
||||||
sendError(callback, status.toString());
|
|
||||||
} else {
|
|
||||||
sendSuccess(callback, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IllegalStateException ex) {
|
|
||||||
// The media player may throw if the session has been killed. For now, we're just catching this here.
|
|
||||||
sendError(callback, "Error playing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pause(final EventCallback callback) {
|
|
||||||
if (!verifySession(callback)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
|
|
||||||
@Override
|
|
||||||
public void onResult(MediaChannelResult result) {
|
|
||||||
Status status = result.getStatus();
|
|
||||||
if (!status.isSuccess()) {
|
|
||||||
debug("Unable to pause: " + status.getStatusCode());
|
|
||||||
sendError(callback, status.toString());
|
|
||||||
} else {
|
|
||||||
sendSuccess(callback, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IllegalStateException ex) {
|
|
||||||
// The media player may throw if the session has been killed. For now, we're just catching this here.
|
|
||||||
sendError(callback, "Error pausing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void end(final EventCallback callback) {
|
|
||||||
if (!verifySession(callback)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback<Status>() {
|
|
||||||
@Override
|
|
||||||
public void onResult(Status result) {
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
try {
|
|
||||||
Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace());
|
|
||||||
remoteMediaPlayer = null;
|
|
||||||
mSessionId = null;
|
|
||||||
apiClient.disconnect();
|
|
||||||
apiClient = null;
|
|
||||||
|
|
||||||
if (callback != null) {
|
|
||||||
sendSuccess(callback, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
debug("Error ending", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callback != null) {
|
|
||||||
sendError(callback, result.getStatus().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IllegalStateException ex) {
|
|
||||||
// The media player may throw if the session has been killed. For now, we're just catching this here.
|
|
||||||
sendError(callback, "Error stopping");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MirrorChannel implements MessageReceivedCallback {
|
|
||||||
/**
|
|
||||||
* @return custom namespace
|
|
||||||
*/
|
|
||||||
public String getNamespace() {
|
|
||||||
return "urn:x-cast:org.mozilla.mirror";
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Receive message from the receiver app
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onMessageReceived(CastDevice castDevice, String namespace,
|
|
||||||
String message) {
|
|
||||||
GeckoAppShell.notifyObservers("MediaPlayer:Response", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendMessage(String message) {
|
|
||||||
if (apiClient != null && mMirrorChannel != null) {
|
|
||||||
try {
|
|
||||||
Cast.CastApi.sendMessage(apiClient, mMirrorChannel.getNamespace(), message)
|
|
||||||
.setResultCallback(
|
|
||||||
new ResultCallback<Status>() {
|
|
||||||
@Override
|
|
||||||
public void onResult(Status result) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "Exception while sending message", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private class MirrorCallback implements ResultCallback<ApplicationConnectionResult> {
|
|
||||||
final EventCallback callback;
|
|
||||||
MirrorCallback(final EventCallback callback) {
|
|
||||||
this.callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResult(ApplicationConnectionResult result) {
|
|
||||||
Status status = result.getStatus();
|
|
||||||
if (status.isSuccess()) {
|
|
||||||
ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
|
|
||||||
mSessionId = result.getSessionId();
|
|
||||||
String applicationStatus = result.getApplicationStatus();
|
|
||||||
boolean wasLaunched = result.getWasLaunched();
|
|
||||||
mApplicationStarted = true;
|
|
||||||
|
|
||||||
// Create the custom message
|
|
||||||
// channel
|
|
||||||
mMirrorChannel = new MirrorChannel();
|
|
||||||
try {
|
|
||||||
Cast.CastApi.setMessageReceivedCallbacks(apiClient,
|
|
||||||
mMirrorChannel
|
|
||||||
.getNamespace(),
|
|
||||||
mMirrorChannel);
|
|
||||||
sendSuccess(callback, null);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(LOGTAG, "Exception while creating channel", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
GeckoAppShell.notifyObservers("Casting:Mirror", route.getId());
|
|
||||||
} else {
|
|
||||||
sendError(callback, status.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void message(String msg, final EventCallback callback) {
|
|
||||||
if (mMirrorChannel != null) {
|
|
||||||
mMirrorChannel.sendMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void mirror(final EventCallback callback) {
|
|
||||||
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
|
|
||||||
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onApplicationStatusChanged() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onVolumeChanged() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onApplicationDisconnected(int errorCode) { }
|
|
||||||
});
|
|
||||||
|
|
||||||
apiClient = new GoogleApiClient.Builder(context)
|
|
||||||
.addApi(Cast.API, apiOptionsBuilder.build())
|
|
||||||
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
|
|
||||||
@Override
|
|
||||||
public void onConnected(Bundle connectionHint) {
|
|
||||||
// Sometimes apiClient is null here. See bug 1061032
|
|
||||||
if (apiClient == null || !apiClient.isConnected()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch the media player app and launch this url once its loaded
|
|
||||||
try {
|
|
||||||
Cast.CastApi.launchApplication(apiClient, MIRROR_RECEIVER_APP_ID, true)
|
|
||||||
.setResultCallback(new MirrorCallback(callback));
|
|
||||||
} catch (Exception e) {
|
|
||||||
debug("Failed to launch application", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionSuspended(int cause) {
|
|
||||||
debug("suspended");
|
|
||||||
}
|
|
||||||
}).build();
|
|
||||||
|
|
||||||
apiClient.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String LOGTAG = "GeckoChromeCastPlayer";
|
|
||||||
private void debug(String msg, Exception e) {
|
|
||||||
if (SHOW_DEBUG) {
|
|
||||||
Log.e(LOGTAG, msg, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void debug(String msg) {
|
|
||||||
if (SHOW_DEBUG) {
|
|
||||||
Log.d(LOGTAG, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,480 +0,0 @@
|
||||||
/* -*- Mode: Java; 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;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.channels.Channels;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
import java.util.zip.GZIPOutputStream;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.CompoundButton;
|
|
||||||
import android.widget.EditText;
|
|
||||||
|
|
||||||
@SuppressLint("Registered") // This activity is only registered in the manifest if MOZ_CRASHREPORTER is set
|
|
||||||
public class CrashReporter extends AppCompatActivity
|
|
||||||
{
|
|
||||||
private static final String LOGTAG = "GeckoCrashReporter";
|
|
||||||
|
|
||||||
private static final String PASSED_MINI_DUMP_KEY = "minidumpPath";
|
|
||||||
private static final String PASSED_MINI_DUMP_SUCCESS_KEY = "minidumpSuccess";
|
|
||||||
private static final String MINI_DUMP_PATH_KEY = "upload_file_minidump";
|
|
||||||
private static final String PAGE_URL_KEY = "URL";
|
|
||||||
private static final String NOTES_KEY = "Notes";
|
|
||||||
private static final String SERVER_URL_KEY = "ServerURL";
|
|
||||||
|
|
||||||
private static final String CRASH_REPORT_SUFFIX = "/mozilla/Crash Reports/";
|
|
||||||
private static final String PENDING_SUFFIX = CRASH_REPORT_SUFFIX + "pending";
|
|
||||||
private static final String SUBMITTED_SUFFIX = CRASH_REPORT_SUFFIX + "submitted";
|
|
||||||
|
|
||||||
private static final String PREFS_SEND_REPORT = "sendReport";
|
|
||||||
private static final String PREFS_INCLUDE_URL = "includeUrl";
|
|
||||||
private static final String PREFS_ALLOW_CONTACT = "allowContact";
|
|
||||||
private static final String PREFS_CONTACT_EMAIL = "contactEmail";
|
|
||||||
|
|
||||||
private Handler mHandler;
|
|
||||||
private ProgressDialog mProgressDialog;
|
|
||||||
private File mPendingMinidumpFile;
|
|
||||||
private File mPendingExtrasFile;
|
|
||||||
private HashMap<String, String> mExtrasStringMap;
|
|
||||||
private boolean mMinidumpSucceeded;
|
|
||||||
|
|
||||||
private boolean moveFile(File inFile, File outFile) {
|
|
||||||
Log.i(LOGTAG, "moving " + inFile + " to " + outFile);
|
|
||||||
if (inFile.renameTo(outFile))
|
|
||||||
return true;
|
|
||||||
try {
|
|
||||||
outFile.createNewFile();
|
|
||||||
Log.i(LOGTAG, "couldn't rename minidump file");
|
|
||||||
// so copy it instead
|
|
||||||
FileChannel inChannel = new FileInputStream(inFile).getChannel();
|
|
||||||
FileChannel outChannel = new FileOutputStream(outFile).getChannel();
|
|
||||||
long transferred = inChannel.transferTo(0, inChannel.size(), outChannel);
|
|
||||||
inChannel.close();
|
|
||||||
outChannel.close();
|
|
||||||
|
|
||||||
if (transferred > 0)
|
|
||||||
inFile.delete();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "exception while copying minidump file: ", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doFinish() {
|
|
||||||
if (mHandler != null) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void finish() {
|
|
||||||
try {
|
|
||||||
if (mProgressDialog.isShowing()) {
|
|
||||||
mProgressDialog.dismiss();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "exception while closing progress dialog: ", e);
|
|
||||||
}
|
|
||||||
super.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
// mHandler is created here so runnables can be run on the main thread
|
|
||||||
mHandler = new Handler();
|
|
||||||
setContentView(R.layout.crash_reporter);
|
|
||||||
mProgressDialog = new ProgressDialog(this);
|
|
||||||
mProgressDialog.setMessage(getString(R.string.sending_crash_report));
|
|
||||||
|
|
||||||
mMinidumpSucceeded = getIntent().getBooleanExtra(PASSED_MINI_DUMP_SUCCESS_KEY, false);
|
|
||||||
if (!mMinidumpSucceeded) {
|
|
||||||
Log.i(LOGTAG, "Failed to get minidump.");
|
|
||||||
}
|
|
||||||
String passedMinidumpPath = getIntent().getStringExtra(PASSED_MINI_DUMP_KEY);
|
|
||||||
File passedMinidumpFile = new File(passedMinidumpPath);
|
|
||||||
File pendingDir = new File(getFilesDir(), PENDING_SUFFIX);
|
|
||||||
pendingDir.mkdirs();
|
|
||||||
mPendingMinidumpFile = new File(pendingDir, passedMinidumpFile.getName());
|
|
||||||
moveFile(passedMinidumpFile, mPendingMinidumpFile);
|
|
||||||
|
|
||||||
File extrasFile = new File(passedMinidumpPath.replaceAll("\\.dmp", ".extra"));
|
|
||||||
mPendingExtrasFile = new File(pendingDir, extrasFile.getName());
|
|
||||||
moveFile(extrasFile, mPendingExtrasFile);
|
|
||||||
|
|
||||||
mExtrasStringMap = new HashMap<String, String>();
|
|
||||||
readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap);
|
|
||||||
|
|
||||||
// Notify GeckoApp that we've crashed, so it can react appropriately during the next start.
|
|
||||||
try {
|
|
||||||
File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED");
|
|
||||||
crashFlag.createNewFile();
|
|
||||||
} catch (GeckoProfileDirectories.NoMozillaDirectoryException | IOException e) {
|
|
||||||
Log.e(LOGTAG, "Cannot set crash flag: ", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
final CheckBox allowContactCheckBox = (CheckBox) findViewById(R.id.allow_contact);
|
|
||||||
final CheckBox includeUrlCheckBox = (CheckBox) findViewById(R.id.include_url);
|
|
||||||
final CheckBox sendReportCheckBox = (CheckBox) findViewById(R.id.send_report);
|
|
||||||
final EditText commentsEditText = (EditText) findViewById(R.id.comment);
|
|
||||||
final EditText emailEditText = (EditText) findViewById(R.id.email);
|
|
||||||
|
|
||||||
// Load CrashReporter preferences to avoid redundant user input.
|
|
||||||
SharedPreferences prefs = GeckoSharedPrefs.forCrashReporter(this);
|
|
||||||
final boolean sendReport = prefs.getBoolean(PREFS_SEND_REPORT, true);
|
|
||||||
final boolean includeUrl = prefs.getBoolean(PREFS_INCLUDE_URL, false);
|
|
||||||
final boolean allowContact = prefs.getBoolean(PREFS_ALLOW_CONTACT, false);
|
|
||||||
final String contactEmail = prefs.getString(PREFS_CONTACT_EMAIL, "");
|
|
||||||
|
|
||||||
allowContactCheckBox.setChecked(allowContact);
|
|
||||||
includeUrlCheckBox.setChecked(includeUrl);
|
|
||||||
sendReportCheckBox.setChecked(sendReport);
|
|
||||||
emailEditText.setText(contactEmail);
|
|
||||||
|
|
||||||
sendReportCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
|
|
||||||
commentsEditText.setEnabled(isChecked);
|
|
||||||
commentsEditText.requestFocus();
|
|
||||||
|
|
||||||
includeUrlCheckBox.setEnabled(isChecked);
|
|
||||||
allowContactCheckBox.setEnabled(isChecked);
|
|
||||||
emailEditText.setEnabled(isChecked && allowContactCheckBox.isChecked());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
allowContactCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
|
|
||||||
// We need to check isEnabled() here because this listener is
|
|
||||||
// fired on rotation -- even when the checkbox is disabled.
|
|
||||||
emailEditText.setEnabled(checkbox.isEnabled() && isChecked);
|
|
||||||
emailEditText.requestFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
emailEditText.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
// Even if the email EditText is disabled, allow it to be
|
|
||||||
// clicked and focused.
|
|
||||||
if (sendReportCheckBox.isChecked() && !v.isEnabled()) {
|
|
||||||
allowContactCheckBox.setChecked(true);
|
|
||||||
v.setEnabled(true);
|
|
||||||
v.requestFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
||||||
builder.setMessage(R.string.crash_closing_alert);
|
|
||||||
builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
CrashReporter.this.finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void backgroundSendReport() {
|
|
||||||
final CheckBox sendReportCheckbox = (CheckBox) findViewById(R.id.send_report);
|
|
||||||
if (!sendReportCheckbox.isChecked()) {
|
|
||||||
doFinish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist settings to avoid redundant user input.
|
|
||||||
savePrefs();
|
|
||||||
|
|
||||||
mProgressDialog.show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
sendReport(mPendingMinidumpFile, mExtrasStringMap, mPendingExtrasFile);
|
|
||||||
}
|
|
||||||
}, "CrashReporter Thread").start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void savePrefs() {
|
|
||||||
SharedPreferences.Editor editor = GeckoSharedPrefs.forCrashReporter(this).edit();
|
|
||||||
|
|
||||||
final boolean allowContact = ((CheckBox) findViewById(R.id.allow_contact)).isChecked();
|
|
||||||
final boolean includeUrl = ((CheckBox) findViewById(R.id.include_url)).isChecked();
|
|
||||||
final boolean sendReport = ((CheckBox) findViewById(R.id.send_report)).isChecked();
|
|
||||||
final String contactEmail = ((EditText) findViewById(R.id.email)).getText().toString();
|
|
||||||
|
|
||||||
editor.putBoolean(PREFS_ALLOW_CONTACT, allowContact);
|
|
||||||
editor.putBoolean(PREFS_INCLUDE_URL, includeUrl);
|
|
||||||
editor.putBoolean(PREFS_SEND_REPORT, sendReport);
|
|
||||||
editor.putString(PREFS_CONTACT_EMAIL, contactEmail);
|
|
||||||
|
|
||||||
// A slight performance improvement via async apply() vs. blocking on commit().
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCloseClick(View v) { // bound via crash_reporter.xml
|
|
||||||
backgroundSendReport();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onRestartClick(View v) { // bound via crash_reporter.xml
|
|
||||||
doRestart();
|
|
||||||
backgroundSendReport();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean readStringsFromFile(String filePath, Map<String, String> stringMap) {
|
|
||||||
try {
|
|
||||||
BufferedReader reader = new BufferedReader(new FileReader(filePath));
|
|
||||||
return readStringsFromReader(reader, stringMap);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "exception while reading strings: ", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean readStringsFromReader(BufferedReader reader, Map<String, String> stringMap) throws IOException {
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
int equalsPos = -1;
|
|
||||||
if ((equalsPos = line.indexOf('=')) != -1) {
|
|
||||||
String key = line.substring(0, equalsPos);
|
|
||||||
String val = unescape(line.substring(equalsPos + 1));
|
|
||||||
stringMap.put(key, val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateBoundary() {
|
|
||||||
// Generate some random numbers to fill out the boundary
|
|
||||||
int r0 = (int)(Integer.MAX_VALUE * Math.random());
|
|
||||||
int r1 = (int)(Integer.MAX_VALUE * Math.random());
|
|
||||||
return String.format("---------------------------%08X%08X", r0, r1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendPart(OutputStream os, String boundary, String name, String data) {
|
|
||||||
try {
|
|
||||||
os.write(("--" + boundary + "\r\n" +
|
|
||||||
"Content-Disposition: form-data; name=\"" + name + "\"\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
data + "\r\n"
|
|
||||||
).getBytes());
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Log.e(LOGTAG, "Exception when sending \"" + name + "\"", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendFile(OutputStream os, String boundary, String name, File file) throws IOException {
|
|
||||||
os.write(("--" + boundary + "\r\n" +
|
|
||||||
"Content-Disposition: form-data; name=\"" + name + "\"; " +
|
|
||||||
"filename=\"" + file.getName() + "\"\r\n" +
|
|
||||||
"Content-Type: application/octet-stream\r\n" +
|
|
||||||
"\r\n"
|
|
||||||
).getBytes());
|
|
||||||
FileChannel fc = new FileInputStream(file).getChannel();
|
|
||||||
fc.transferTo(0, fc.size(), Channels.newChannel(os));
|
|
||||||
fc.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String readLogcat() {
|
|
||||||
final String crashReporterProc = " " + android.os.Process.myPid() + ' ';
|
|
||||||
BufferedReader br = null;
|
|
||||||
try {
|
|
||||||
// get at most the last 400 lines of logcat
|
|
||||||
Process proc = Runtime.getRuntime().exec(new String[] {
|
|
||||||
"logcat", "-v", "threadtime", "-t", "400", "-d", "*:D"
|
|
||||||
});
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
|
|
||||||
for (String s = br.readLine(); s != null; s = br.readLine()) {
|
|
||||||
if (s.contains(crashReporterProc)) {
|
|
||||||
// Don't include logs from the crash reporter's process.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sb.append(s).append('\n');
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "Unable to get logcat: " + e.toString();
|
|
||||||
} finally {
|
|
||||||
if (br != null) {
|
|
||||||
try {
|
|
||||||
br.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendReport(File minidumpFile, Map<String, String> extras, File extrasFile) {
|
|
||||||
Log.i(LOGTAG, "sendReport: " + minidumpFile.getPath());
|
|
||||||
final CheckBox includeURLCheckbox = (CheckBox) findViewById(R.id.include_url);
|
|
||||||
|
|
||||||
String spec = extras.get(SERVER_URL_KEY);
|
|
||||||
if (spec == null) {
|
|
||||||
doFinish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(LOGTAG, "server url: " + spec);
|
|
||||||
try {
|
|
||||||
URL url = new URL(spec);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
String boundary = generateBoundary();
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
|
|
||||||
conn.setRequestProperty("Content-Encoding", "gzip");
|
|
||||||
|
|
||||||
OutputStream os = new GZIPOutputStream(conn.getOutputStream());
|
|
||||||
for (String key : extras.keySet()) {
|
|
||||||
if (key.equals(PAGE_URL_KEY)) {
|
|
||||||
if (includeURLCheckbox.isChecked())
|
|
||||||
sendPart(os, boundary, key, extras.get(key));
|
|
||||||
} else if (!key.equals(SERVER_URL_KEY) && !key.equals(NOTES_KEY)) {
|
|
||||||
sendPart(os, boundary, key, extras.get(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add some extra information to notes so its displayed by
|
|
||||||
// crash-stats.mozilla.org. Remove this when bug 607942 is fixed.
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append(extras.containsKey(NOTES_KEY) ? extras.get(NOTES_KEY) + "\n" : "");
|
|
||||||
if (AppConstants.MOZ_MIN_CPU_VERSION < 7) {
|
|
||||||
sb.append("nothumb Build\n");
|
|
||||||
}
|
|
||||||
sb.append(Build.MANUFACTURER).append(' ')
|
|
||||||
.append(Build.MODEL).append('\n')
|
|
||||||
.append(Build.FINGERPRINT);
|
|
||||||
sendPart(os, boundary, NOTES_KEY, sb.toString());
|
|
||||||
|
|
||||||
sendPart(os, boundary, "Min_ARM_Version", Integer.toString(AppConstants.MOZ_MIN_CPU_VERSION));
|
|
||||||
sendPart(os, boundary, "Android_Manufacturer", Build.MANUFACTURER);
|
|
||||||
sendPart(os, boundary, "Android_Model", Build.MODEL);
|
|
||||||
sendPart(os, boundary, "Android_Board", Build.BOARD);
|
|
||||||
sendPart(os, boundary, "Android_Brand", Build.BRAND);
|
|
||||||
sendPart(os, boundary, "Android_Device", Build.DEVICE);
|
|
||||||
sendPart(os, boundary, "Android_Display", Build.DISPLAY);
|
|
||||||
sendPart(os, boundary, "Android_Fingerprint", Build.FINGERPRINT);
|
|
||||||
sendPart(os, boundary, "Android_APP_ABI", AppConstants.MOZ_APP_ABI);
|
|
||||||
sendPart(os, boundary, "Android_CPU_ABI", Build.CPU_ABI);
|
|
||||||
sendPart(os, boundary, "Android_MIN_SDK", Integer.toString(AppConstants.Versions.MIN_SDK_VERSION));
|
|
||||||
sendPart(os, boundary, "Android_MAX_SDK", Integer.toString(AppConstants.Versions.MAX_SDK_VERSION));
|
|
||||||
try {
|
|
||||||
sendPart(os, boundary, "Android_CPU_ABI2", Build.CPU_ABI2);
|
|
||||||
sendPart(os, boundary, "Android_Hardware", Build.HARDWARE);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Log.e(LOGTAG, "Exception while sending SDK version 8 keys", ex);
|
|
||||||
}
|
|
||||||
sendPart(os, boundary, "Android_Version", Build.VERSION.SDK_INT + " (" + Build.VERSION.CODENAME + ")");
|
|
||||||
if (Versions.feature16Plus && includeURLCheckbox.isChecked()) {
|
|
||||||
sendPart(os, boundary, "Android_Logcat", readLogcat());
|
|
||||||
}
|
|
||||||
|
|
||||||
String comment = ((EditText) findViewById(R.id.comment)).getText().toString();
|
|
||||||
if (!TextUtils.isEmpty(comment)) {
|
|
||||||
sendPart(os, boundary, "Comments", comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (((CheckBox) findViewById(R.id.allow_contact)).isChecked()) {
|
|
||||||
String email = ((EditText) findViewById(R.id.email)).getText().toString();
|
|
||||||
sendPart(os, boundary, "Email", email);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPart(os, boundary, PASSED_MINI_DUMP_SUCCESS_KEY, mMinidumpSucceeded ? "True" : "False");
|
|
||||||
sendFile(os, boundary, MINI_DUMP_PATH_KEY, minidumpFile);
|
|
||||||
os.write(("\r\n--" + boundary + "--\r\n").getBytes());
|
|
||||||
os.flush();
|
|
||||||
os.close();
|
|
||||||
BufferedReader br = new BufferedReader(
|
|
||||||
new InputStreamReader(conn.getInputStream()));
|
|
||||||
HashMap<String, String> responseMap = new HashMap<String, String>();
|
|
||||||
readStringsFromReader(br, responseMap);
|
|
||||||
|
|
||||||
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
|
||||||
File submittedDir = new File(getFilesDir(),
|
|
||||||
SUBMITTED_SUFFIX);
|
|
||||||
submittedDir.mkdirs();
|
|
||||||
minidumpFile.delete();
|
|
||||||
extrasFile.delete();
|
|
||||||
String crashid = responseMap.get("CrashID");
|
|
||||||
File file = new File(submittedDir, crashid + ".txt");
|
|
||||||
FileOutputStream fos = new FileOutputStream(file);
|
|
||||||
fos.write("Crash ID: ".getBytes());
|
|
||||||
fos.write(crashid.getBytes());
|
|
||||||
fos.close();
|
|
||||||
} else {
|
|
||||||
Log.i(LOGTAG, "Received failure HTTP response code from server: " + conn.getResponseCode());
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(LOGTAG, "exception during send: ", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
doFinish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doRestart() {
|
|
||||||
try {
|
|
||||||
String action = "android.intent.action.MAIN";
|
|
||||||
Intent intent = new Intent(action);
|
|
||||||
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
|
|
||||||
AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
|
||||||
intent.putExtra("didRestart", true);
|
|
||||||
Log.i(LOGTAG, intent.toString());
|
|
||||||
startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "error while trying to restart", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String unescape(String string) {
|
|
||||||
return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import org.mozilla.gecko.widget.themed.ThemedEditText;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
public class CustomEditText extends ThemedEditText {
|
|
||||||
private OnKeyPreImeListener mOnKeyPreImeListener;
|
|
||||||
private OnSelectionChangedListener mOnSelectionChangedListener;
|
|
||||||
private OnWindowFocusChangeListener mOnWindowFocusChangeListener;
|
|
||||||
private int mHighlightColor;
|
|
||||||
|
|
||||||
public CustomEditText(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
setPrivateMode(false); // Initialize mHighlightColor.
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnKeyPreImeListener {
|
|
||||||
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnKeyPreImeListener(OnKeyPreImeListener listener) {
|
|
||||||
mOnKeyPreImeListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
|
||||||
if (mOnKeyPreImeListener != null)
|
|
||||||
return mOnKeyPreImeListener.onKeyPreIme(this, keyCode, event);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnSelectionChangedListener {
|
|
||||||
public void onSelectionChanged(int selStart, int selEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
|
|
||||||
mOnSelectionChangedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSelectionChanged(int selStart, int selEnd) {
|
|
||||||
if (mOnSelectionChangedListener != null)
|
|
||||||
mOnSelectionChangedListener.onSelectionChanged(selStart, selEnd);
|
|
||||||
|
|
||||||
super.onSelectionChanged(selStart, selEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnWindowFocusChangeListener {
|
|
||||||
public void onWindowFocusChanged(boolean hasFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnWindowFocusChangeListener(OnWindowFocusChangeListener listener) {
|
|
||||||
mOnWindowFocusChangeListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWindowFocusChanged(boolean hasFocus) {
|
|
||||||
super.onWindowFocusChanged(hasFocus);
|
|
||||||
if (mOnWindowFocusChangeListener != null)
|
|
||||||
mOnWindowFocusChangeListener.onWindowFocusChanged(hasFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide a getHighlightColor implementation for API level < 16.
|
|
||||||
@Override
|
|
||||||
public int getHighlightColor() {
|
|
||||||
return mHighlightColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setPrivateMode(boolean isPrivate) {
|
|
||||||
super.setPrivateMode(isPrivate);
|
|
||||||
|
|
||||||
mHighlightColor = ContextCompat.getColor(getContext(), isPrivate
|
|
||||||
? R.color.url_bar_text_highlight_pb : R.color.fennec_ui_orange);
|
|
||||||
// android:textColorHighlight cannot support a ColorStateList.
|
|
||||||
setHighlightColor(mHighlightColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
|
||||||
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.support.v4.app.NotificationCompat;
|
|
||||||
import android.text.Spannable;
|
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.style.StyleSpan;
|
|
||||||
|
|
||||||
public class DataReportingNotification {
|
|
||||||
|
|
||||||
private static final String LOGTAG = "DataReportNotification";
|
|
||||||
|
|
||||||
public static final String ALERT_NAME_DATAREPORTING_NOTIFICATION = "datareporting-notification";
|
|
||||||
|
|
||||||
private static final String PREFS_POLICY_NOTIFIED_TIME = "datareporting.policy.dataSubmissionPolicyNotifiedTime";
|
|
||||||
private static final String PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion";
|
|
||||||
private static final int DATA_REPORTING_VERSION = 2;
|
|
||||||
|
|
||||||
public static void checkAndNotifyPolicy(Context context) {
|
|
||||||
SharedPreferences dataPrefs = GeckoSharedPrefs.forApp(context);
|
|
||||||
final int currentVersion = dataPrefs.getInt(PREFS_POLICY_VERSION, -1);
|
|
||||||
|
|
||||||
if (currentVersion < 1) {
|
|
||||||
// This is a first run, so notify user about data policy.
|
|
||||||
notifyDataPolicy(context, dataPrefs);
|
|
||||||
|
|
||||||
// If healthreport is enabled, set default preference value.
|
|
||||||
if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
|
|
||||||
SharedPreferences.Editor editor = dataPrefs.edit();
|
|
||||||
editor.putBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentVersion == 1) {
|
|
||||||
// Redisplay notification only for Beta because version 2 updates Beta policy and update version.
|
|
||||||
if (TextUtils.equals("beta", AppConstants.MOZ_UPDATE_CHANNEL)) {
|
|
||||||
notifyDataPolicy(context, dataPrefs);
|
|
||||||
} else {
|
|
||||||
// Silently update the version.
|
|
||||||
SharedPreferences.Editor editor = dataPrefs.edit();
|
|
||||||
editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentVersion >= DATA_REPORTING_VERSION) {
|
|
||||||
// Do nothing, we're at a current (or future) version.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch a notification of the data policy, and record notification time and version.
|
|
||||||
*/
|
|
||||||
public static void notifyDataPolicy(Context context, SharedPreferences sharedPrefs) {
|
|
||||||
boolean result = false;
|
|
||||||
try {
|
|
||||||
// Launch main App to launch Data choices when notification is clicked.
|
|
||||||
Intent prefIntent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
|
|
||||||
prefIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
|
||||||
|
|
||||||
GeckoPreferences.setResourceToOpen(prefIntent, "preferences_privacy");
|
|
||||||
prefIntent.putExtra(ALERT_NAME_DATAREPORTING_NOTIFICATION, true);
|
|
||||||
|
|
||||||
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
final Resources resources = context.getResources();
|
|
||||||
|
|
||||||
// Create and send notification.
|
|
||||||
String notificationTitle = resources.getString(R.string.datareporting_notification_title);
|
|
||||||
String notificationSummary;
|
|
||||||
if (Versions.preJB) {
|
|
||||||
notificationSummary = resources.getString(R.string.datareporting_notification_action);
|
|
||||||
} else {
|
|
||||||
// Display partial version of Big Style notification for supporting devices.
|
|
||||||
notificationSummary = resources.getString(R.string.datareporting_notification_summary);
|
|
||||||
}
|
|
||||||
String notificationAction = resources.getString(R.string.datareporting_notification_action);
|
|
||||||
String notificationBigSummary = resources.getString(R.string.datareporting_notification_summary);
|
|
||||||
|
|
||||||
// Make styled ticker text for display in notification bar.
|
|
||||||
String tickerString = resources.getString(R.string.datareporting_notification_ticker_text);
|
|
||||||
SpannableString tickerText = new SpannableString(tickerString);
|
|
||||||
// Bold the notification title of the ticker text, which is the same string as notificationTitle.
|
|
||||||
tickerText.setSpan(new StyleSpan(Typeface.BOLD), 0, notificationTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
|
||||||
|
|
||||||
Notification notification = new NotificationCompat.Builder(context)
|
|
||||||
.setContentTitle(notificationTitle)
|
|
||||||
.setContentText(notificationSummary)
|
|
||||||
.setSmallIcon(R.drawable.ic_status_logo)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(contentIntent)
|
|
||||||
.setStyle(new NotificationCompat.BigTextStyle()
|
|
||||||
.bigText(notificationBigSummary))
|
|
||||||
.addAction(R.drawable.firefox_settings_alert, notificationAction, contentIntent)
|
|
||||||
.setTicker(tickerText)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
int notificationID = ALERT_NAME_DATAREPORTING_NOTIFICATION.hashCode();
|
|
||||||
notificationManager.notify(notificationID, notification);
|
|
||||||
|
|
||||||
// Record version and notification time.
|
|
||||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
editor.putLong(PREFS_POLICY_NOTIFIED_TIME, now);
|
|
||||||
editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
|
|
||||||
editor.apply();
|
|
||||||
result = true;
|
|
||||||
} finally {
|
|
||||||
// We want to track any errors, so record notification outcome.
|
|
||||||
Telemetry.sendUIEvent(TelemetryContract.Event.POLICY_NOTIFICATION_SUCCESS, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.util.Log;
|
|
||||||
import org.mozilla.gecko.util.ActivityResultHandler;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
import org.mozilla.gecko.util.InputOptionsUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Supports the DevTools WiFi debugging authentication flow by invoking a QR decoder.
|
|
||||||
*/
|
|
||||||
public class DevToolsAuthHelper {
|
|
||||||
|
|
||||||
private static final String LOGTAG = "GeckoDevToolsAuthHelper";
|
|
||||||
|
|
||||||
public static void scan(Context context, final EventCallback callback) {
|
|
||||||
final Intent intent = InputOptionsUtils.createQRCodeReaderIntent();
|
|
||||||
|
|
||||||
intent.putExtra("PROMPT_MESSAGE", context.getString(R.string.devtools_auth_scan_header));
|
|
||||||
|
|
||||||
// Check ahead of time if an activity exists for the intent. This
|
|
||||||
// avoids a case where we get both an ActivityNotFoundException *and*
|
|
||||||
// an activity result when the activity is missing.
|
|
||||||
PackageManager pm = context.getPackageManager();
|
|
||||||
if (pm.resolveActivity(intent, 0) == null) {
|
|
||||||
Log.w(LOGTAG, "PackageManager can't resolve the activity.");
|
|
||||||
callback.sendError("PackageManager can't resolve the activity.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityHandlerHelper.startIntent(intent, new ActivityResultHandler() {
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int resultCode, Intent intent) {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
String text = intent.getStringExtra("SCAN_RESULT");
|
|
||||||
callback.sendSuccess(text);
|
|
||||||
} else {
|
|
||||||
callback.sendError(resultCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,361 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.widget.PopupWindow;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
import org.mozilla.gecko.util.GeckoEventListener;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
import org.mozilla.gecko.widget.AnchoredPopup;
|
|
||||||
import org.mozilla.gecko.widget.DoorHanger;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import org.mozilla.gecko.widget.DoorhangerConfig;
|
|
||||||
|
|
||||||
public class DoorHangerPopup extends AnchoredPopup
|
|
||||||
implements GeckoEventListener,
|
|
||||||
Tabs.OnTabsChangedListener,
|
|
||||||
PopupWindow.OnDismissListener,
|
|
||||||
DoorHanger.OnButtonClickListener {
|
|
||||||
private static final String LOGTAG = "GeckoDoorHangerPopup";
|
|
||||||
|
|
||||||
// Stores a set of all active DoorHanger notifications. A DoorHanger is
|
|
||||||
// uniquely identified by its tabId and value.
|
|
||||||
private final HashSet<DoorHanger> mDoorHangers;
|
|
||||||
|
|
||||||
// Whether or not the doorhanger popup is disabled.
|
|
||||||
private boolean mDisabled;
|
|
||||||
|
|
||||||
public DoorHangerPopup(Context context) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
mDoorHangers = new HashSet<DoorHanger>();
|
|
||||||
|
|
||||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
|
|
||||||
"Doorhanger:Add",
|
|
||||||
"Doorhanger:Remove");
|
|
||||||
Tabs.registerOnTabsChangedListener(this);
|
|
||||||
|
|
||||||
setOnDismissListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void destroy() {
|
|
||||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
|
|
||||||
"Doorhanger:Add",
|
|
||||||
"Doorhanger:Remove");
|
|
||||||
Tabs.unregisterOnTabsChangedListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporarily disables the doorhanger popup. If the popup is disabled,
|
|
||||||
* it will not be shown to the user, but it will continue to process
|
|
||||||
* calls to add/remove doorhanger notifications.
|
|
||||||
*/
|
|
||||||
void disable() {
|
|
||||||
mDisabled = true;
|
|
||||||
updatePopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-enables the doorhanger popup.
|
|
||||||
*/
|
|
||||||
void enable() {
|
|
||||||
mDisabled = false;
|
|
||||||
updatePopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(String event, JSONObject geckoObject) {
|
|
||||||
try {
|
|
||||||
if (event.equals("Doorhanger:Add")) {
|
|
||||||
final DoorhangerConfig config = makeConfigFromJSON(geckoObject);
|
|
||||||
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
addDoorHanger(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (event.equals("Doorhanger:Remove")) {
|
|
||||||
final int tabId = geckoObject.getInt("tabID");
|
|
||||||
final String value = geckoObject.getString("value");
|
|
||||||
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
DoorHanger doorHanger = getDoorHanger(tabId, value);
|
|
||||||
if (doorHanger == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
removeDoorHanger(doorHanger);
|
|
||||||
updatePopup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private DoorhangerConfig makeConfigFromJSON(JSONObject json) throws JSONException {
|
|
||||||
final int tabId = json.getInt("tabID");
|
|
||||||
final String id = json.getString("value");
|
|
||||||
|
|
||||||
final String typeString = json.optString("category");
|
|
||||||
DoorHanger.Type doorhangerType = DoorHanger.Type.DEFAULT;
|
|
||||||
if (DoorHanger.Type.LOGIN.toString().equals(typeString)) {
|
|
||||||
doorhangerType = DoorHanger.Type.LOGIN;
|
|
||||||
} else if (DoorHanger.Type.GEOLOCATION.toString().equals(typeString)) {
|
|
||||||
doorhangerType = DoorHanger.Type.GEOLOCATION;
|
|
||||||
} else if (DoorHanger.Type.DESKTOPNOTIFICATION2.toString().equals(typeString)) {
|
|
||||||
doorhangerType = DoorHanger.Type.DESKTOPNOTIFICATION2;
|
|
||||||
} else if (DoorHanger.Type.WEBRTC.toString().equals(typeString)) {
|
|
||||||
doorhangerType = DoorHanger.Type.WEBRTC;
|
|
||||||
} else if (DoorHanger.Type.VIBRATION.toString().equals(typeString)) {
|
|
||||||
doorhangerType = DoorHanger.Type.VIBRATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
final DoorhangerConfig config = new DoorhangerConfig(tabId, id, doorhangerType, this);
|
|
||||||
|
|
||||||
config.setMessage(json.getString("message"));
|
|
||||||
config.setOptions(json.getJSONObject("options"));
|
|
||||||
|
|
||||||
final JSONArray buttonArray = json.getJSONArray("buttons");
|
|
||||||
int numButtons = buttonArray.length();
|
|
||||||
if (numButtons > 2) {
|
|
||||||
Log.e(LOGTAG, "Doorhanger can have a maximum of two buttons!");
|
|
||||||
numButtons = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < numButtons; i++) {
|
|
||||||
final JSONObject buttonJSON = buttonArray.getJSONObject(i);
|
|
||||||
final boolean isPositive = buttonJSON.optBoolean("positive", false);
|
|
||||||
config.setButton(buttonJSON.getString("label"), buttonJSON.getInt("callback"), isPositive);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This callback is automatically executed on the UI thread.
|
|
||||||
@Override
|
|
||||||
public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) {
|
|
||||||
switch (msg) {
|
|
||||||
case CLOSED:
|
|
||||||
// Remove any doorhangers for a tab when it's closed (make
|
|
||||||
// a temporary set to avoid a ConcurrentModificationException)
|
|
||||||
removeTabDoorHangers(tab.getId(), true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LOCATION_CHANGE:
|
|
||||||
// Only remove doorhangers if the popup is hidden or if we're navigating to a new URL
|
|
||||||
if (!isShowing() || !data.equals(tab.getURL()))
|
|
||||||
removeTabDoorHangers(tab.getId(), false);
|
|
||||||
|
|
||||||
// Update the popup if the location change was on the current tab
|
|
||||||
if (Tabs.getInstance().isSelectedTab(tab))
|
|
||||||
updatePopup();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SELECTED:
|
|
||||||
// Always update the popup when a new tab is selected. This will cover cases
|
|
||||||
// where a different tab was closed, since we always need to select a new tab.
|
|
||||||
updatePopup();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a doorhanger.
|
|
||||||
*
|
|
||||||
* This method must be called on the UI thread.
|
|
||||||
*/
|
|
||||||
void addDoorHanger(DoorhangerConfig config) {
|
|
||||||
final int tabId = config.getTabId();
|
|
||||||
// Don't add a doorhanger for a tab that doesn't exist
|
|
||||||
if (Tabs.getInstance().getTab(tabId) == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the doorhanger if it already exists
|
|
||||||
DoorHanger oldDoorHanger = getDoorHanger(tabId, config.getId());
|
|
||||||
if (oldDoorHanger != null) {
|
|
||||||
removeDoorHanger(oldDoorHanger);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mInflated) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
final DoorHanger newDoorHanger = DoorHanger.Get(mContext, config);
|
|
||||||
|
|
||||||
mDoorHangers.add(newDoorHanger);
|
|
||||||
mContent.addView(newDoorHanger);
|
|
||||||
|
|
||||||
// Only update the popup if we're adding a notification to the selected tab
|
|
||||||
if (tabId == Tabs.getInstance().getSelectedTab().getId())
|
|
||||||
updatePopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* DoorHanger.OnButtonClickListener implementation
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
|
|
||||||
GeckoAppShell.notifyObservers("Doorhanger:Reply", response.toString());
|
|
||||||
removeDoorHanger(doorhanger);
|
|
||||||
updatePopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a doorhanger.
|
|
||||||
*
|
|
||||||
* This method must be called on the UI thread.
|
|
||||||
*/
|
|
||||||
DoorHanger getDoorHanger(int tabId, String value) {
|
|
||||||
for (DoorHanger dh : mDoorHangers) {
|
|
||||||
if (dh.getTabId() == tabId && dh.getIdentifier().equals(value))
|
|
||||||
return dh;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no doorhanger for the given tabId and value, return null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a doorhanger.
|
|
||||||
*
|
|
||||||
* This method must be called on the UI thread.
|
|
||||||
*/
|
|
||||||
void removeDoorHanger(final DoorHanger doorHanger) {
|
|
||||||
mDoorHangers.remove(doorHanger);
|
|
||||||
mContent.removeView(doorHanger);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes doorhangers for a given tab.
|
|
||||||
* @param tabId identifier of the tab to remove doorhangers from
|
|
||||||
* @param forceRemove boolean for force-removing tabs. If true, all doorhangers associated
|
|
||||||
* with the tab specified are removed; if false, only remove the doorhangers
|
|
||||||
* that are not persistent, as specified by the doorhanger options.
|
|
||||||
*
|
|
||||||
* This method must be called on the UI thread.
|
|
||||||
*/
|
|
||||||
void removeTabDoorHangers(int tabId, boolean forceRemove) {
|
|
||||||
// Make a temporary set to avoid a ConcurrentModificationException
|
|
||||||
HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>();
|
|
||||||
for (DoorHanger dh : mDoorHangers) {
|
|
||||||
// Only remove transient doorhangers for the given tab
|
|
||||||
if (dh.getTabId() == tabId
|
|
||||||
&& (forceRemove || (!forceRemove && dh.shouldRemove(isShowing())))) {
|
|
||||||
doorHangersToRemove.add(dh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (DoorHanger dh : doorHangersToRemove) {
|
|
||||||
removeDoorHanger(dh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the popup state.
|
|
||||||
*
|
|
||||||
* This method must be called on the UI thread.
|
|
||||||
*/
|
|
||||||
void updatePopup() {
|
|
||||||
// Bail if the selected tab is null, if there are no active doorhangers,
|
|
||||||
// if we haven't inflated the layout yet (this can happen if updatePopup()
|
|
||||||
// is called before the runnable from addDoorHanger() runs), or if the
|
|
||||||
// doorhanger popup is temporarily disabled.
|
|
||||||
Tab tab = Tabs.getInstance().getSelectedTab();
|
|
||||||
if (tab == null || mDoorHangers.size() == 0 || !mInflated || mDisabled) {
|
|
||||||
dismiss();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show doorhangers for the selected tab
|
|
||||||
int tabId = tab.getId();
|
|
||||||
boolean shouldShowPopup = false;
|
|
||||||
DoorHanger firstDoorhanger = null;
|
|
||||||
for (DoorHanger dh : mDoorHangers) {
|
|
||||||
if (dh.getTabId() == tabId) {
|
|
||||||
dh.setVisibility(View.VISIBLE);
|
|
||||||
shouldShowPopup = true;
|
|
||||||
if (firstDoorhanger == null) {
|
|
||||||
firstDoorhanger = dh;
|
|
||||||
} else {
|
|
||||||
dh.hideTitle();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dh.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dismiss the popup if there are no doorhangers to show for this tab
|
|
||||||
if (!shouldShowPopup) {
|
|
||||||
dismiss();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDividers();
|
|
||||||
|
|
||||||
final String baseDomain = tab.getBaseDomain();
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(baseDomain)) {
|
|
||||||
firstDoorhanger.hideTitle();
|
|
||||||
} else {
|
|
||||||
firstDoorhanger.showTitle(tab.getFavicon(), baseDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isShowing()) {
|
|
||||||
show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFocusable(true);
|
|
||||||
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Show all inter-DoorHanger dividers (ie. Dividers on all visible DoorHangers except the last one)
|
|
||||||
private void showDividers() {
|
|
||||||
int count = mContent.getChildCount();
|
|
||||||
DoorHanger lastVisibleDoorHanger = null;
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
DoorHanger dh = (DoorHanger) mContent.getChildAt(i);
|
|
||||||
dh.showDivider();
|
|
||||||
if (dh.getVisibility() == View.VISIBLE) {
|
|
||||||
lastVisibleDoorHanger = dh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lastVisibleDoorHanger != null) {
|
|
||||||
lastVisibleDoorHanger.hideDivider();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDismiss() {
|
|
||||||
final int tabId = Tabs.getInstance().getSelectedTab().getId();
|
|
||||||
removeTabDoorHangers(tabId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dismiss() {
|
|
||||||
// If the popup is focusable while it is hidden, we run into crashes
|
|
||||||
// on pre-ICS devices when the popup gets focus before it is shown.
|
|
||||||
setFocusable(false);
|
|
||||||
super.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,235 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
import org.mozilla.gecko.permissions.Permissions;
|
|
||||||
import org.mozilla.gecko.util.NativeEventListener;
|
|
||||||
import org.mozilla.gecko.util.NativeJSObject;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.IllegalArgumentException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import android.app.DownloadManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.media.MediaScannerConnection;
|
|
||||||
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class DownloadsIntegration implements NativeEventListener
|
|
||||||
{
|
|
||||||
private static final String LOGTAG = "GeckoDownloadsIntegration";
|
|
||||||
|
|
||||||
private static final List<String> UNKNOWN_MIME_TYPES;
|
|
||||||
static {
|
|
||||||
final ArrayList<String> tempTypes = new ArrayList<>(3);
|
|
||||||
tempTypes.add("unknown/unknown"); // This will be used as a default mime type for unknown files
|
|
||||||
tempTypes.add("application/unknown");
|
|
||||||
tempTypes.add("application/octet-stream"); // Github uses this for APK files
|
|
||||||
UNKNOWN_MIME_TYPES = Collections.unmodifiableList(tempTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String DOWNLOAD_REMOVE = "Download:Remove";
|
|
||||||
|
|
||||||
private DownloadsIntegration() {
|
|
||||||
EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this, DOWNLOAD_REMOVE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DownloadsIntegration sInstance;
|
|
||||||
|
|
||||||
private static class Download {
|
|
||||||
final File file;
|
|
||||||
final long id;
|
|
||||||
|
|
||||||
final private static int UNKNOWN_ID = -1;
|
|
||||||
|
|
||||||
public Download(final String path) {
|
|
||||||
this(path, UNKNOWN_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Download(final String path, final long id) {
|
|
||||||
file = new File(path);
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Download fromJSON(final NativeJSObject obj) {
|
|
||||||
final String path = obj.getString("path");
|
|
||||||
return new Download(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Download fromCursor(final Cursor c) {
|
|
||||||
final String path = c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
|
|
||||||
final long id = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
|
|
||||||
return new Download(path, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean equals(final Download download) {
|
|
||||||
return file.equals(download.file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void init() {
|
|
||||||
if (sInstance == null) {
|
|
||||||
sInstance = new DownloadsIntegration();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(final String event, final NativeJSObject message,
|
|
||||||
final EventCallback callback) {
|
|
||||||
if (DOWNLOAD_REMOVE.equals(event)) {
|
|
||||||
final Download d = Download.fromJSON(message);
|
|
||||||
removeDownload(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean useSystemDownloadManager() {
|
|
||||||
if (!AppConstants.ANDROID_DOWNLOADS_INTEGRATION) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int state = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
|
|
||||||
try {
|
|
||||||
state = GeckoAppShell.getContext().getPackageManager().getApplicationEnabledSetting("com.android.providers.downloads");
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// Download Manager package does not exist
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (PackageManager.COMPONENT_ENABLED_STATE_ENABLED == state ||
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT == state);
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI(calledFrom = "gecko")
|
|
||||||
public static String getTemporaryDownloadDirectory() {
|
|
||||||
Context context = GeckoAppShell.getApplicationContext();
|
|
||||||
|
|
||||||
if (Permissions.has(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
|
||||||
// We do have the STORAGE permission, so we can save the file directly to the public
|
|
||||||
// downloads directory.
|
|
||||||
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
.getAbsolutePath();
|
|
||||||
} else {
|
|
||||||
// Without the permission we are going to start to download the file to the cache
|
|
||||||
// directory. Later in the process we will ask for the permission and the download
|
|
||||||
// process will move the file to the actual downloads directory. If we do not get the
|
|
||||||
// permission then the download will be cancelled.
|
|
||||||
return context.getCacheDir().getAbsolutePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@WrapForJNI(calledFrom = "gecko")
|
|
||||||
public static void scanMedia(final String aFile, String aMimeType) {
|
|
||||||
String mimeType = aMimeType;
|
|
||||||
if (UNKNOWN_MIME_TYPES.contains(mimeType)) {
|
|
||||||
// If this is a generic undefined mimetype, erase it so that we can try to determine
|
|
||||||
// one from the file extension below.
|
|
||||||
mimeType = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the platform didn't give us a mimetype, try to guess one from the filename
|
|
||||||
if (TextUtils.isEmpty(mimeType)) {
|
|
||||||
final int extPosition = aFile.lastIndexOf(".");
|
|
||||||
if (extPosition > 0 && extPosition < aFile.length() - 1) {
|
|
||||||
mimeType = GeckoAppShell.getMimeTypeFromExtension(aFile.substring(extPosition + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addCompletedDownload will throw if it received any null parameters. Use aMimeType or a default
|
|
||||||
// if we still don't have one.
|
|
||||||
if (TextUtils.isEmpty(mimeType)) {
|
|
||||||
if (TextUtils.isEmpty(aMimeType)) {
|
|
||||||
mimeType = UNKNOWN_MIME_TYPES.get(0);
|
|
||||||
} else {
|
|
||||||
mimeType = aMimeType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useSystemDownloadManager()) {
|
|
||||||
final File f = new File(aFile);
|
|
||||||
final DownloadManager dm = (DownloadManager) GeckoAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
|
||||||
dm.addCompletedDownload(f.getName(),
|
|
||||||
f.getName(),
|
|
||||||
true, // Media scanner should scan this
|
|
||||||
mimeType,
|
|
||||||
f.getAbsolutePath(),
|
|
||||||
Math.max(1, f.length()), // Some versions of Android require downloads to be at least length 1
|
|
||||||
false); // Don't show a notification.
|
|
||||||
} else {
|
|
||||||
final Context context = GeckoAppShell.getContext();
|
|
||||||
final GeckoMediaScannerClient client = new GeckoMediaScannerClient(context, aFile, mimeType);
|
|
||||||
client.connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void removeDownload(final Download download) {
|
|
||||||
if (!useSystemDownloadManager()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final DownloadManager dm = (DownloadManager) GeckoAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
|
||||||
|
|
||||||
Cursor c = null;
|
|
||||||
try {
|
|
||||||
c = dm.query((new DownloadManager.Query()).setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
|
|
||||||
if (c == null || !c.moveToFirst()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
final Download d = Download.fromCursor(c);
|
|
||||||
// Try hard as we can to verify this download is the one we think it is
|
|
||||||
if (download.equals(d)) {
|
|
||||||
dm.remove(d.id);
|
|
||||||
}
|
|
||||||
} while (c.moveToNext());
|
|
||||||
} finally {
|
|
||||||
if (c != null) {
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class GeckoMediaScannerClient implements MediaScannerConnectionClient {
|
|
||||||
private final String mFile;
|
|
||||||
private final String mMimeType;
|
|
||||||
private MediaScannerConnection mScanner;
|
|
||||||
|
|
||||||
public GeckoMediaScannerClient(Context context, String file, String mimeType) {
|
|
||||||
mFile = file;
|
|
||||||
mMimeType = mimeType;
|
|
||||||
mScanner = new MediaScannerConnection(context, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void connect() {
|
|
||||||
mScanner.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMediaScannerConnected() {
|
|
||||||
mScanner.scanFile(mFile, mMimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScanCompleted(String path, Uri uri) {
|
|
||||||
if (path.equals(mFile)) {
|
|
||||||
mScanner.disconnect();
|
|
||||||
mScanner = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
package org.mozilla.gecko;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.PrefsHelper.PrefHandlerBase;
|
|
||||||
import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
|
|
||||||
import org.mozilla.gecko.gfx.LayerView;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class DynamicToolbar {
|
|
||||||
private static final String LOGTAG = "DynamicToolbar";
|
|
||||||
|
|
||||||
private static final String STATE_ENABLED = "dynamic_toolbar";
|
|
||||||
private static final String CHROME_PREF = "browser.chrome.dynamictoolbar";
|
|
||||||
|
|
||||||
// DynamicToolbar is enabled iff prefEnabled is true *and* accessibilityEnabled is false,
|
|
||||||
// so it is disabled by default on startup. We do not enable it until we explicitly get
|
|
||||||
// the pref from Gecko telling us to turn it on.
|
|
||||||
private volatile boolean prefEnabled;
|
|
||||||
private boolean accessibilityEnabled;
|
|
||||||
// On some device we have to force-disable the dynamic toolbar because of
|
|
||||||
// bugs in the Android code. See bug 1231554.
|
|
||||||
private final boolean forceDisabled;
|
|
||||||
|
|
||||||
private final PrefsHelper.PrefHandler prefObserver;
|
|
||||||
private LayerView layerView;
|
|
||||||
private OnEnabledChangedListener enabledChangedListener;
|
|
||||||
private boolean temporarilyVisible;
|
|
||||||
|
|
||||||
public enum VisibilityTransition {
|
|
||||||
IMMEDIATE,
|
|
||||||
ANIMATE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for changes to the dynamic toolbar's enabled state.
|
|
||||||
*/
|
|
||||||
public interface OnEnabledChangedListener {
|
|
||||||
/**
|
|
||||||
* This callback is executed on the UI thread.
|
|
||||||
*/
|
|
||||||
public void onEnabledChanged(boolean enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DynamicToolbar() {
|
|
||||||
// Listen to the dynamic toolbar pref
|
|
||||||
prefObserver = new PrefHandler();
|
|
||||||
PrefsHelper.addObserver(new String[] { CHROME_PREF }, prefObserver);
|
|
||||||
forceDisabled = isForceDisabled();
|
|
||||||
if (forceDisabled) {
|
|
||||||
Log.i(LOGTAG, "Force-disabling dynamic toolbar for " + Build.MODEL + " (" + Build.DEVICE + "/" + Build.PRODUCT + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isForceDisabled() {
|
|
||||||
// Force-disable dynamic toolbar on the variants of the Galaxy Note 10.1
|
|
||||||
// and Note 8.0 running Android 4.1.2. (Bug 1231554). This includes
|
|
||||||
// the following model numbers:
|
|
||||||
// GT-N8000, GT-N8005, GT-N8010, GT-N8013, GT-N8020
|
|
||||||
// GT-N5100, GT-N5110, GT-N5120
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN
|
|
||||||
&& (Build.MODEL.startsWith("GT-N80") ||
|
|
||||||
Build.MODEL.startsWith("GT-N51"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Also disable variants of the Galaxy Note 4 on Android 5.0.1 (Bug 1301593)
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
|
|
||||||
&& (Build.MODEL.startsWith("SM-N910"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void destroy() {
|
|
||||||
PrefsHelper.removeObserver(prefObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLayerView(LayerView layerView) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
this.layerView = layerView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnabledChangedListener(OnEnabledChangedListener listener) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
enabledChangedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
outState.putBoolean(STATE_ENABLED, prefEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onRestoreInstanceState(Bundle savedInstanceState) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
prefEnabled = savedInstanceState.getBoolean(STATE_ENABLED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
if (forceDisabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prefEnabled && !accessibilityEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAccessibilityEnabled(boolean enabled) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
if (accessibilityEnabled == enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable the dynamic toolbar when accessibility features are enabled,
|
|
||||||
// and re-read the preference when they're disabled.
|
|
||||||
accessibilityEnabled = enabled;
|
|
||||||
if (prefEnabled) {
|
|
||||||
triggerEnabledListener();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVisible(boolean visible, VisibilityTransition transition) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
if (layerView == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't hide the ActionBar/Toolbar, if it's pinned open by TextSelection.
|
|
||||||
if (visible == false &&
|
|
||||||
layerView.getDynamicToolbarAnimator().isPinnedBy(PinReason.ACTION_MODE)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final boolean isImmediate = transition == VisibilityTransition.IMMEDIATE;
|
|
||||||
if (visible) {
|
|
||||||
layerView.getDynamicToolbarAnimator().showToolbar(isImmediate);
|
|
||||||
} else {
|
|
||||||
layerView.getDynamicToolbarAnimator().hideToolbar(isImmediate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTemporarilyVisible(boolean visible, VisibilityTransition transition) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
if (layerView == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visible == temporarilyVisible) {
|
|
||||||
// nothing to do
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
temporarilyVisible = visible;
|
|
||||||
final boolean isImmediate = transition == VisibilityTransition.IMMEDIATE;
|
|
||||||
if (visible) {
|
|
||||||
layerView.getDynamicToolbarAnimator().showToolbar(isImmediate);
|
|
||||||
} else {
|
|
||||||
layerView.getDynamicToolbarAnimator().hideToolbar(isImmediate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void persistTemporaryVisibility() {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
if (temporarilyVisible) {
|
|
||||||
temporarilyVisible = false;
|
|
||||||
setVisible(true, VisibilityTransition.IMMEDIATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPinned(boolean pinned, PinReason reason) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
if (layerView == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
layerView.getDynamicToolbarAnimator().setPinned(pinned, reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void triggerEnabledListener() {
|
|
||||||
if (enabledChangedListener != null) {
|
|
||||||
enabledChangedListener.onEnabledChanged(isEnabled());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PrefHandler extends PrefHandlerBase {
|
|
||||||
@Override
|
|
||||||
public void prefValue(String pref, boolean value) {
|
|
||||||
if (value == prefEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
prefEnabled = value;
|
|
||||||
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// If accessibility is enabled, the dynamic toolbar is
|
|
||||||
// forced to be off.
|
|
||||||
if (!accessibilityEnabled) {
|
|
||||||
triggerEnabledListener();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
|
||||||
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
import org.mozilla.gecko.util.UIAsyncTask;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.support.design.widget.Snackbar;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.EditText;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A dialog that allows editing a bookmarks url, title, or keywords
|
|
||||||
* <p>
|
|
||||||
* Invoked by calling one of the {@link org.mozilla.gecko.EditBookmarkDialog#show(String)}
|
|
||||||
* methods.
|
|
||||||
*/
|
|
||||||
public class EditBookmarkDialog {
|
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
public EditBookmarkDialog(Context context) {
|
|
||||||
mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A private struct to make it easier to pass bookmark data across threads
|
|
||||||
*/
|
|
||||||
private class Bookmark {
|
|
||||||
final int id;
|
|
||||||
final String title;
|
|
||||||
final String url;
|
|
||||||
final String keyword;
|
|
||||||
|
|
||||||
public Bookmark(int aId, String aTitle, String aUrl, String aKeyword) {
|
|
||||||
id = aId;
|
|
||||||
title = aTitle;
|
|
||||||
url = aUrl;
|
|
||||||
keyword = aKeyword;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This text watcher to enable or disable the OK button if the dialog contains
|
|
||||||
* valid information. This class is overridden to do data checking on different fields.
|
|
||||||
* By itself, it always enables the button.
|
|
||||||
*
|
|
||||||
* Callers can also assign a paired partner to the TextWatcher, and callers will check
|
|
||||||
* that both are enabled before enabling the ok button.
|
|
||||||
*/
|
|
||||||
private class EditBookmarkTextWatcher implements TextWatcher {
|
|
||||||
// A stored reference to the dialog containing the text field being watched
|
|
||||||
protected AlertDialog mDialog;
|
|
||||||
|
|
||||||
// A stored text watcher to do the real verification of a field
|
|
||||||
protected EditBookmarkTextWatcher mPairedTextWatcher;
|
|
||||||
|
|
||||||
// Whether or not the ok button should be enabled.
|
|
||||||
protected boolean mEnabled = true;
|
|
||||||
|
|
||||||
public EditBookmarkTextWatcher(AlertDialog aDialog) {
|
|
||||||
mDialog = aDialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPairedTextWatcher(EditBookmarkTextWatcher aTextWatcher) {
|
|
||||||
mPairedTextWatcher = aTextWatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return mEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Textwatcher interface
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
// Disable if the we're disabled or the paired partner is disabled
|
|
||||||
boolean enabled = mEnabled && (mPairedTextWatcher == null || mPairedTextWatcher.isEnabled());
|
|
||||||
mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s) {}
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A version of the EditBookmarkTextWatcher for the url field of the dialog.
|
|
||||||
* Only checks if the field is empty or not.
|
|
||||||
*/
|
|
||||||
private class LocationTextWatcher extends EditBookmarkTextWatcher {
|
|
||||||
public LocationTextWatcher(AlertDialog aDialog) {
|
|
||||||
super(aDialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disables the ok button if the location field is empty.
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
mEnabled = (s.toString().trim().length() > 0);
|
|
||||||
super.onTextChanged(s, start, before, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A version of the EditBookmarkTextWatcher for the keyword field of the dialog.
|
|
||||||
* Checks if the field has any (non leading or trailing) spaces.
|
|
||||||
*/
|
|
||||||
private class KeywordTextWatcher extends EditBookmarkTextWatcher {
|
|
||||||
public KeywordTextWatcher(AlertDialog aDialog) {
|
|
||||||
super(aDialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
// Disable if the keyword contains spaces
|
|
||||||
mEnabled = (s.toString().trim().indexOf(' ') == -1);
|
|
||||||
super.onTextChanged(s, start, before, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the Edit bookmark dialog for a particular url. If the url is bookmarked multiple times
|
|
||||||
* this will just edit the first instance it finds.
|
|
||||||
*
|
|
||||||
* @param url The url of the bookmark to edit. The dialog will look up other information like the id,
|
|
||||||
* current title, or keywords associated with this url. If the url isn't bookmarked, the
|
|
||||||
* dialog will fail silently. If the url is bookmarked multiple times, this will only show
|
|
||||||
* information about the first it finds.
|
|
||||||
*/
|
|
||||||
public void show(final String url) {
|
|
||||||
final ContentResolver cr = mContext.getContentResolver();
|
|
||||||
final BrowserDB db = BrowserDB.from(mContext);
|
|
||||||
(new UIAsyncTask.WithoutParams<Bookmark>(ThreadUtils.getBackgroundHandler()) {
|
|
||||||
@Override
|
|
||||||
public Bookmark doInBackground() {
|
|
||||||
final Cursor cursor = db.getBookmarkForUrl(cr, url);
|
|
||||||
if (cursor == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bookmark bookmark = null;
|
|
||||||
try {
|
|
||||||
cursor.moveToFirst();
|
|
||||||
bookmark = new Bookmark(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)),
|
|
||||||
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE)),
|
|
||||||
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)),
|
|
||||||
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.KEYWORD)));
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
return bookmark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPostExecute(Bookmark bookmark) {
|
|
||||||
if (bookmark == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
show(bookmark.id, bookmark.title, bookmark.url, bookmark.keyword);
|
|
||||||
}
|
|
||||||
}).execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the Edit bookmark dialog for a set of data. This will show the dialog whether
|
|
||||||
* a bookmark with this url exists or not, but the results will NOT be saved if the id
|
|
||||||
* is not a valid bookmark id.
|
|
||||||
*
|
|
||||||
* @param id The id of the bookmark to change. If there is no bookmark with this ID, the dialog
|
|
||||||
* will fail silently.
|
|
||||||
* @param title The initial title to show in the dialog
|
|
||||||
* @param url The initial url to show in the dialog
|
|
||||||
* @param keyword The initial keyword to show in the dialog
|
|
||||||
*/
|
|
||||||
public void show(final int id, final String title, final String url, final String keyword) {
|
|
||||||
final Context context = mContext;
|
|
||||||
|
|
||||||
AlertDialog.Builder editPrompt = new AlertDialog.Builder(context);
|
|
||||||
final View editView = LayoutInflater.from(context).inflate(R.layout.bookmark_edit, null);
|
|
||||||
editPrompt.setTitle(R.string.bookmark_edit_title);
|
|
||||||
editPrompt.setView(editView);
|
|
||||||
|
|
||||||
final EditText nameText = ((EditText) editView.findViewById(R.id.edit_bookmark_name));
|
|
||||||
final EditText locationText = ((EditText) editView.findViewById(R.id.edit_bookmark_location));
|
|
||||||
final EditText keywordText = ((EditText) editView.findViewById(R.id.edit_bookmark_keyword));
|
|
||||||
nameText.setText(title);
|
|
||||||
locationText.setText(url);
|
|
||||||
keywordText.setText(keyword);
|
|
||||||
|
|
||||||
final BrowserDB db = BrowserDB.from(mContext);
|
|
||||||
editPrompt.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int whichButton) {
|
|
||||||
(new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
|
|
||||||
@Override
|
|
||||||
public Void doInBackground() {
|
|
||||||
String newUrl = locationText.getText().toString().trim();
|
|
||||||
String newKeyword = keywordText.getText().toString().trim();
|
|
||||||
|
|
||||||
db.updateBookmark(context.getContentResolver(), id, newUrl, nameText.getText().toString(), newKeyword);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPostExecute(Void result) {
|
|
||||||
SnackbarBuilder.builder((Activity) context)
|
|
||||||
.message(R.string.bookmark_updated)
|
|
||||||
.duration(Snackbar.LENGTH_LONG)
|
|
||||||
.buildAndShow();
|
|
||||||
}
|
|
||||||
}).execute();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
editPrompt.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int whichButton) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final AlertDialog dialog = editPrompt.create();
|
|
||||||
|
|
||||||
// Create our TextWatchers
|
|
||||||
LocationTextWatcher locationTextWatcher = new LocationTextWatcher(dialog);
|
|
||||||
KeywordTextWatcher keywordTextWatcher = new KeywordTextWatcher(dialog);
|
|
||||||
|
|
||||||
// Cross reference the TextWatchers
|
|
||||||
locationTextWatcher.setPairedTextWatcher(keywordTextWatcher);
|
|
||||||
keywordTextWatcher.setPairedTextWatcher(locationTextWatcher);
|
|
||||||
|
|
||||||
// Add the TextWatcher Listeners
|
|
||||||
locationText.addTextChangedListener(locationTextWatcher);
|
|
||||||
keywordText.addTextChangedListener(keywordTextWatcher);
|
|
||||||
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.keepsafe.switchboard.Preferences;
|
|
||||||
import com.keepsafe.switchboard.SwitchBoard;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class should reflect the experiment names found in the Switchboard experiments config here:
|
|
||||||
* https://github.com/mozilla-services/switchboard-experiments
|
|
||||||
*/
|
|
||||||
public class Experiments {
|
|
||||||
private static final String LOGTAG = "GeckoExperiments";
|
|
||||||
|
|
||||||
// Show a system notification linking to a "What's New" page on app update.
|
|
||||||
public static final String WHATSNEW_NOTIFICATION = "whatsnew-notification";
|
|
||||||
|
|
||||||
// Subscribe to known, bookmarked sites and show a notification if new content is available.
|
|
||||||
public static final String CONTENT_NOTIFICATIONS_12HRS = "content-notifications-12hrs";
|
|
||||||
public static final String CONTENT_NOTIFICATIONS_8AM = "content-notifications-8am";
|
|
||||||
public static final String CONTENT_NOTIFICATIONS_5PM = "content-notifications-5pm";
|
|
||||||
|
|
||||||
// Onboarding: "Features and Story". These experiments are determined
|
|
||||||
// on the client, they are not part of the server config.
|
|
||||||
public static final String ONBOARDING3_A = "onboarding3-a"; // Control: No first run
|
|
||||||
public static final String ONBOARDING3_B = "onboarding3-b"; // 4 static Feature + 1 dynamic slides
|
|
||||||
public static final String ONBOARDING3_C = "onboarding3-c"; // Differentiating features slides
|
|
||||||
|
|
||||||
// Synchronizing the catalog of downloadable content from Kinto
|
|
||||||
public static final String DOWNLOAD_CONTENT_CATALOG_SYNC = "download-content-catalog-sync";
|
|
||||||
|
|
||||||
// Promotion for "Add to homescreen"
|
|
||||||
public static final String PROMOTE_ADD_TO_HOMESCREEN = "promote-add-to-homescreen";
|
|
||||||
|
|
||||||
public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
|
|
||||||
|
|
||||||
// Promotion to bookmark reader-view items after entering reader view three times (Bug 1247689)
|
|
||||||
public static final String TRIPLE_READERVIEW_BOOKMARK_PROMPT = "triple-readerview-bookmark-prompt";
|
|
||||||
|
|
||||||
// Only show origin in URL bar instead of full URL (Bug 1236431)
|
|
||||||
public static final String URLBAR_SHOW_ORIGIN_ONLY = "urlbar-show-origin-only";
|
|
||||||
|
|
||||||
// Show name of organization (EV cert) instead of full URL in URL bar (Bug 1249594).
|
|
||||||
public static final String URLBAR_SHOW_EV_CERT_OWNER = "urlbar-show-ev-cert-owner";
|
|
||||||
|
|
||||||
// Play HLS videos in a VideoView (Bug 1313391)
|
|
||||||
public static final String HLS_VIDEO_PLAYBACK = "hls-video-playback";
|
|
||||||
|
|
||||||
// Make new activity stream panel available (to replace top sites) (Bug 1313316)
|
|
||||||
public static final String ACTIVITY_STREAM = "activity-stream";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns if a user is in certain local experiment.
|
|
||||||
* @param experiment Name of experiment to look up
|
|
||||||
* @return returns value for experiment or false if experiment does not exist.
|
|
||||||
*/
|
|
||||||
public static boolean isInExperimentLocal(Context context, String experiment) {
|
|
||||||
if (SwitchBoard.isInBucket(context, 0, 20)) {
|
|
||||||
return Experiments.ONBOARDING3_A.equals(experiment);
|
|
||||||
} else if (SwitchBoard.isInBucket(context, 20, 60)) {
|
|
||||||
return Experiments.ONBOARDING3_B.equals(experiment);
|
|
||||||
} else if (SwitchBoard.isInBucket(context, 60, 100)) {
|
|
||||||
return Experiments.ONBOARDING3_C.equals(experiment);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns list of all active experiments, remote and local.
|
|
||||||
* @return List of experiment names Strings
|
|
||||||
*/
|
|
||||||
public static List<String> getActiveExperiments(Context c) {
|
|
||||||
final List<String> experiments = new LinkedList<>();
|
|
||||||
experiments.addAll(SwitchBoard.getActiveExperiments(c));
|
|
||||||
|
|
||||||
// Add onboarding version.
|
|
||||||
final String onboardingExperiment = GeckoSharedPrefs.forProfile(c).getString(Experiments.PREF_ONBOARDING_VERSION, null);
|
|
||||||
if (!TextUtils.isEmpty(onboardingExperiment)) {
|
|
||||||
experiments.add(onboardingExperiment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return experiments;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets an override to force an experiment to be enabled or disabled. This value
|
|
||||||
* will be read and used before reading the switchboard server configuration.
|
|
||||||
*
|
|
||||||
* @param c Context
|
|
||||||
* @param experimentName Experiment name
|
|
||||||
* @param isEnabled Whether or not the experiment should be enabled
|
|
||||||
*/
|
|
||||||
public static void setOverride(Context c, String experimentName, boolean isEnabled) {
|
|
||||||
Log.d(LOGTAG, "setOverride: " + experimentName + " = " + isEnabled);
|
|
||||||
Preferences.setOverrideValue(c, experimentName, isEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the override value for an experiment.
|
|
||||||
*
|
|
||||||
* @param c Context
|
|
||||||
* @param experimentName Experiment name
|
|
||||||
*/
|
|
||||||
public static void clearOverride(Context c, String experimentName) {
|
|
||||||
Log.d(LOGTAG, "clearOverride: " + experimentName);
|
|
||||||
Preferences.clearOverrideValue(c, experimentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.GeckoAppShell;
|
|
||||||
import org.mozilla.gecko.util.GeckoEventListener;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class FilePicker implements GeckoEventListener {
|
|
||||||
private static final String LOGTAG = "GeckoFilePicker";
|
|
||||||
private static FilePicker sFilePicker;
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
public interface ResultHandler {
|
|
||||||
public void gotFile(String filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void init(Context context) {
|
|
||||||
if (sFilePicker == null) {
|
|
||||||
sFilePicker = new FilePicker(context.getApplicationContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected FilePicker(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
EventDispatcher.getInstance().registerGeckoThreadListener(this, "FilePicker:Show");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(String event, final JSONObject message) {
|
|
||||||
if (event.equals("FilePicker:Show")) {
|
|
||||||
String mimeType = "*/*";
|
|
||||||
final String mode = message.optString("mode");
|
|
||||||
final int tabId = message.optInt("tabId", -1);
|
|
||||||
final String title = message.optString("title");
|
|
||||||
|
|
||||||
if ("mimeType".equals(mode))
|
|
||||||
mimeType = message.optString("mimeType");
|
|
||||||
else if ("extension".equals(mode))
|
|
||||||
mimeType = GeckoAppShell.getMimeTypeFromExtensions(message.optString("extensions"));
|
|
||||||
|
|
||||||
showFilePickerAsync(title, mimeType, new ResultHandler() {
|
|
||||||
@Override
|
|
||||||
public void gotFile(String filename) {
|
|
||||||
try {
|
|
||||||
message.put("file", filename);
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
Log.i(LOGTAG, "Can't add filename to message " + filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
GeckoAppShell.notifyObservers("FilePicker:Result", message.toString());
|
|
||||||
}
|
|
||||||
}, tabId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addActivities(Intent intent, HashMap<String, Intent> intents, HashMap<String, Intent> filters) {
|
|
||||||
PackageManager pm = context.getPackageManager();
|
|
||||||
List<ResolveInfo> lri = pm.queryIntentActivities(intent, 0);
|
|
||||||
for (ResolveInfo ri : lri) {
|
|
||||||
ComponentName cn = new ComponentName(ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name);
|
|
||||||
if (filters != null && !filters.containsKey(cn.toString())) {
|
|
||||||
Intent rintent = new Intent(intent);
|
|
||||||
rintent.setComponent(cn);
|
|
||||||
intents.put(cn.toString(), rintent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Intent getIntent(String mimeType) {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
||||||
intent.setType(mimeType);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Intent> getIntentsForFilePicker(final String mimeType,
|
|
||||||
final FilePickerResultHandler fileHandler) {
|
|
||||||
// The base intent to use for the file picker. Even if this is an implicit intent, Android will
|
|
||||||
// still show a list of Activities that match this action/type.
|
|
||||||
Intent baseIntent;
|
|
||||||
// A HashMap of Activities the base intent will show in the chooser. This is used
|
|
||||||
// to filter activities from other intents so that we don't show duplicates.
|
|
||||||
HashMap<String, Intent> baseIntents = new HashMap<String, Intent>();
|
|
||||||
// A list of other activities to shwo in the picker (and the intents to launch them).
|
|
||||||
HashMap<String, Intent> intents = new HashMap<String, Intent> ();
|
|
||||||
|
|
||||||
if ("audio/*".equals(mimeType)) {
|
|
||||||
// For audio the only intent is the mimetype
|
|
||||||
baseIntent = getIntent(mimeType);
|
|
||||||
addActivities(baseIntent, baseIntents, null);
|
|
||||||
} else if ("image/*".equals(mimeType)) {
|
|
||||||
// For images the base is a capture intent
|
|
||||||
baseIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
|
||||||
baseIntent.putExtra(MediaStore.EXTRA_OUTPUT,
|
|
||||||
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
|
|
||||||
fileHandler.generateImageName())));
|
|
||||||
addActivities(baseIntent, baseIntents, null);
|
|
||||||
|
|
||||||
// We also add the mimetype intent
|
|
||||||
addActivities(getIntent(mimeType), intents, baseIntents);
|
|
||||||
} else if ("video/*".equals(mimeType)) {
|
|
||||||
// For videos the base is a capture intent
|
|
||||||
baseIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
|
||||||
addActivities(baseIntent, baseIntents, null);
|
|
||||||
|
|
||||||
// We also add the mimetype intent
|
|
||||||
addActivities(getIntent(mimeType), intents, baseIntents);
|
|
||||||
} else {
|
|
||||||
baseIntent = getIntent("*/*");
|
|
||||||
addActivities(baseIntent, baseIntents, null);
|
|
||||||
|
|
||||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
|
||||||
intent.putExtra(MediaStore.EXTRA_OUTPUT,
|
|
||||||
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
|
|
||||||
fileHandler.generateImageName())));
|
|
||||||
addActivities(intent, intents, baseIntents);
|
|
||||||
intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
|
||||||
addActivities(intent, intents, baseIntents);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't find any activities, we fall back to the */* mimetype intent
|
|
||||||
if (baseIntents.size() == 0 && intents.size() == 0) {
|
|
||||||
intents.clear();
|
|
||||||
|
|
||||||
baseIntent = getIntent("*/*");
|
|
||||||
addActivities(baseIntent, baseIntents, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayList<Intent> vals = new ArrayList<Intent>(intents.values());
|
|
||||||
vals.add(0, baseIntent);
|
|
||||||
return vals;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getFilePickerTitle(String mimeType) {
|
|
||||||
if (mimeType.equals("audio/*")) {
|
|
||||||
return context.getString(R.string.filepicker_audio_title);
|
|
||||||
} else if (mimeType.equals("image/*")) {
|
|
||||||
return context.getString(R.string.filepicker_image_title);
|
|
||||||
} else if (mimeType.equals("video/*")) {
|
|
||||||
return context.getString(R.string.filepicker_video_title);
|
|
||||||
} else {
|
|
||||||
return context.getString(R.string.filepicker_title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface IntentHandler {
|
|
||||||
public void gotIntent(Intent intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Gets an intent that can open a particular mimetype. Will show a prompt with a list
|
|
||||||
* of Activities that can handle the mietype. Asynchronously calls the handler when
|
|
||||||
* one of the intents is selected. If the caller passes in null for the handler, will still
|
|
||||||
* prompt for the activity, but will throw away the result.
|
|
||||||
*/
|
|
||||||
private void getFilePickerIntentAsync(String title,
|
|
||||||
final String mimeType,
|
|
||||||
final FilePickerResultHandler fileHandler,
|
|
||||||
final IntentHandler handler) {
|
|
||||||
List<Intent> intents = getIntentsForFilePicker(mimeType, fileHandler);
|
|
||||||
|
|
||||||
if (intents.size() == 0) {
|
|
||||||
Log.i(LOGTAG, "no activities for the file picker!");
|
|
||||||
handler.gotIntent(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent base = intents.remove(0);
|
|
||||||
|
|
||||||
if (intents.size() == 0) {
|
|
||||||
handler.gotIntent(base);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(title)) {
|
|
||||||
title = getFilePickerTitle(mimeType);
|
|
||||||
}
|
|
||||||
Intent chooser = Intent.createChooser(base, title);
|
|
||||||
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[intents.size()]));
|
|
||||||
handler.gotIntent(chooser);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
|
|
||||||
* sends the file returned to the passed in handler. If a null handler is passed in, will still
|
|
||||||
* pick and launch the file picker, but will throw away the result.
|
|
||||||
*/
|
|
||||||
protected void showFilePickerAsync(final String title, final String mimeType, final ResultHandler handler, final int tabId) {
|
|
||||||
final FilePickerResultHandler fileHandler = new FilePickerResultHandler(handler, context, tabId);
|
|
||||||
getFilePickerIntentAsync(title, mimeType, fileHandler, new IntentHandler() {
|
|
||||||
@Override
|
|
||||||
public void gotIntent(Intent intent) {
|
|
||||||
if (handler == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
handler.gotFile("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityHandlerHelper.startIntent(intent, fileHandler);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,282 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.util.ActivityResultHandler;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.os.Process;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.provider.OpenableColumns;
|
|
||||||
import android.support.v4.app.FragmentActivity;
|
|
||||||
import android.support.v4.app.LoaderManager;
|
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
|
||||||
import android.support.v4.content.CursorLoader;
|
|
||||||
import android.support.v4.content.Loader;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.format.Time;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
class FilePickerResultHandler implements ActivityResultHandler {
|
|
||||||
private static final String LOGTAG = "GeckoFilePickerResultHandler";
|
|
||||||
private static final String UPLOADS_DIR = "uploads";
|
|
||||||
|
|
||||||
private final FilePicker.ResultHandler handler;
|
|
||||||
private final int tabId;
|
|
||||||
private final File cacheDir;
|
|
||||||
|
|
||||||
// this code is really hacky and doesn't belong anywhere so I'm putting it here for now
|
|
||||||
// until I can come up with a better solution.
|
|
||||||
private String mImageName = "";
|
|
||||||
|
|
||||||
/* Use this constructor to asynchronously listen for results */
|
|
||||||
public FilePickerResultHandler(final FilePicker.ResultHandler handler, final Context context, final int tabId) {
|
|
||||||
this.tabId = tabId;
|
|
||||||
this.cacheDir = new File(context.getCacheDir(), UPLOADS_DIR);
|
|
||||||
this.handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendResult(String res) {
|
|
||||||
if (handler != null) {
|
|
||||||
handler.gotFile(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int resultCode, Intent intent) {
|
|
||||||
if (resultCode != Activity.RESULT_OK) {
|
|
||||||
sendResult("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Camera results won't return an Intent. Use the file name we passed to the original intent.
|
|
||||||
// In Android M, camera results return an empty Intent rather than null.
|
|
||||||
if (intent == null || (intent.getAction() == null && intent.getData() == null)) {
|
|
||||||
if (mImageName != null) {
|
|
||||||
File file = new File(Environment.getExternalStorageDirectory(), mImageName);
|
|
||||||
sendResult(file.getAbsolutePath());
|
|
||||||
} else {
|
|
||||||
sendResult("");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri uri = intent.getData();
|
|
||||||
if (uri == null) {
|
|
||||||
sendResult("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some file pickers may return a file uri
|
|
||||||
if ("file".equals(uri.getScheme())) {
|
|
||||||
String path = uri.getPath();
|
|
||||||
sendResult(path == null ? "" : path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
|
|
||||||
final LoaderManager lm = fa.getSupportLoaderManager();
|
|
||||||
|
|
||||||
// Finally, Video pickers and some file pickers may return a content provider.
|
|
||||||
final ContentResolver cr = fa.getContentResolver();
|
|
||||||
final Cursor cursor = cr.query(uri, new String[] { MediaStore.Video.Media.DATA }, null, null, null);
|
|
||||||
if (cursor != null) {
|
|
||||||
try {
|
|
||||||
// Try a query to make sure the expected columns exist
|
|
||||||
int index = cursor.getColumnIndex(MediaStore.Video.Media.DATA);
|
|
||||||
if (index >= 0) {
|
|
||||||
lm.initLoader(intent.hashCode(), null, new VideoLoaderCallbacks(uri));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
// We'll try a different loader below
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateImageName() {
|
|
||||||
Time now = new Time();
|
|
||||||
now.setToNow();
|
|
||||||
mImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg";
|
|
||||||
return mImageName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class VideoLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
|
||||||
final private Uri uri;
|
|
||||||
public VideoLoaderCallbacks(Uri uri) {
|
|
||||||
this.uri = uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
|
||||||
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
|
|
||||||
return new CursorLoader(fa,
|
|
||||||
uri,
|
|
||||||
new String[] { MediaStore.Video.Media.DATA },
|
|
||||||
null, // selection
|
|
||||||
null, // selectionArgs
|
|
||||||
null); // sortOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
String res = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));
|
|
||||||
|
|
||||||
// Some pickers (the KitKat Documents one for instance) won't return a temporary file here.
|
|
||||||
// Fall back to the normal FileLoader if we didn't find anything.
|
|
||||||
if (TextUtils.isEmpty(res)) {
|
|
||||||
tryFileLoaderCallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendResult(res);
|
|
||||||
} else {
|
|
||||||
tryFileLoaderCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void tryFileLoaderCallback() {
|
|
||||||
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
|
|
||||||
final LoaderManager lm = fa.getSupportLoaderManager();
|
|
||||||
lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(Loader<Cursor> loader) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class's only dependency on FilePickerResultHandler is sendResult.
|
|
||||||
*/
|
|
||||||
private class FileLoaderCallbacks implements LoaderCallbacks<Cursor>,
|
|
||||||
Tabs.OnTabsChangedListener {
|
|
||||||
private final Uri uri;
|
|
||||||
private final File cacheDir;
|
|
||||||
private final int tabId;
|
|
||||||
String tempFile;
|
|
||||||
|
|
||||||
public FileLoaderCallbacks(Uri uri, File cacheDir, int tabId) {
|
|
||||||
this.uri = uri;
|
|
||||||
this.cacheDir = cacheDir;
|
|
||||||
this.tabId = tabId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
|
||||||
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
|
|
||||||
return new CursorLoader(fa,
|
|
||||||
uri,
|
|
||||||
new String[] { OpenableColumns.DISPLAY_NAME },
|
|
||||||
null, // selection
|
|
||||||
null, // selectionArgs
|
|
||||||
null); // sortOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
String name = cursor.getString(0);
|
|
||||||
// tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
|
|
||||||
String fileName = "tmp_" + Process.myPid() + "-";
|
|
||||||
String fileExt;
|
|
||||||
int period;
|
|
||||||
|
|
||||||
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
|
|
||||||
final ContentResolver cr = fa.getContentResolver();
|
|
||||||
|
|
||||||
// Generate an extension if we don't already have one
|
|
||||||
if (name == null || (period = name.lastIndexOf('.')) == -1) {
|
|
||||||
String mimeType = cr.getType(uri);
|
|
||||||
fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType);
|
|
||||||
} else {
|
|
||||||
fileExt = name.substring(period);
|
|
||||||
fileName += name.substring(0, period);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now write the data to the temp file
|
|
||||||
FileOutputStream fos = null;
|
|
||||||
try {
|
|
||||||
cacheDir.mkdir();
|
|
||||||
|
|
||||||
File file = File.createTempFile(fileName, fileExt, cacheDir);
|
|
||||||
fos = new FileOutputStream(file);
|
|
||||||
InputStream is = cr.openInputStream(uri);
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int len = is.read(buf);
|
|
||||||
while (len != -1) {
|
|
||||||
fos.write(buf, 0, len);
|
|
||||||
len = is.read(buf);
|
|
||||||
}
|
|
||||||
fos.close();
|
|
||||||
is.close();
|
|
||||||
tempFile = file.getAbsolutePath();
|
|
||||||
sendResult((tempFile == null) ? "" : tempFile);
|
|
||||||
|
|
||||||
if (tabId > -1 && !TextUtils.isEmpty(tempFile)) {
|
|
||||||
Tabs.registerOnTabsChangedListener(this);
|
|
||||||
}
|
|
||||||
} catch (IOException ex) {
|
|
||||||
Log.i(LOGTAG, "Error writing file", ex);
|
|
||||||
} finally {
|
|
||||||
if (fos != null) {
|
|
||||||
try {
|
|
||||||
fos.close();
|
|
||||||
} catch (IOException e) { /* not much to do here */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sendResult("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(Loader<Cursor> loader) { }
|
|
||||||
|
|
||||||
/*Tabs.OnTabsChangedListener*/
|
|
||||||
// This cleans up our temp file. If it doesn't run, we just hope that Android
|
|
||||||
// will eventually does the cleanup for us.
|
|
||||||
@Override
|
|
||||||
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
|
|
||||||
if ((tab == null) || (tab.getId() != tabId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg == Tabs.TabEvents.LOCATION_CHANGE ||
|
|
||||||
msg == Tabs.TabEvents.CLOSED) {
|
|
||||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
File f = new File(tempFile);
|
|
||||||
f.delete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tabs' listener array is safe to modify during use: its
|
|
||||||
// iteration pattern is based on snapshots.
|
|
||||||
Tabs.unregisterOnTabsChangedListener(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,256 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.util.GeckoEventListener;
|
|
||||||
import org.mozilla.gecko.util.GeckoRequest;
|
|
||||||
import org.mozilla.gecko.util.NativeJSObject;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnClickListener, GeckoEventListener {
|
|
||||||
private static final String LOGTAG = "GeckoFindInPageBar";
|
|
||||||
private static final String REQUEST_ID = "FindInPageBar";
|
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
private CustomEditText mFindText;
|
|
||||||
private TextView mStatusText;
|
|
||||||
private boolean mInflated;
|
|
||||||
|
|
||||||
public FindInPageBar(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
mContext = context;
|
|
||||||
setFocusable(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void inflateContent() {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
|
||||||
View content = inflater.inflate(R.layout.find_in_page_content, this);
|
|
||||||
|
|
||||||
content.findViewById(R.id.find_prev).setOnClickListener(this);
|
|
||||||
content.findViewById(R.id.find_next).setOnClickListener(this);
|
|
||||||
content.findViewById(R.id.find_close).setOnClickListener(this);
|
|
||||||
|
|
||||||
// Capture clicks on the rest of the view to prevent them from
|
|
||||||
// leaking into other views positioned below.
|
|
||||||
content.setOnClickListener(this);
|
|
||||||
|
|
||||||
mFindText = (CustomEditText) content.findViewById(R.id.find_text);
|
|
||||||
mFindText.addTextChangedListener(this);
|
|
||||||
mFindText.setOnKeyPreImeListener(new CustomEditText.OnKeyPreImeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
hide();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mStatusText = (TextView) content.findViewById(R.id.find_status);
|
|
||||||
|
|
||||||
mInflated = true;
|
|
||||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
|
|
||||||
"FindInPage:MatchesCountResult",
|
|
||||||
"TextSelection:Data");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show() {
|
|
||||||
if (!mInflated)
|
|
||||||
inflateContent();
|
|
||||||
|
|
||||||
setVisibility(VISIBLE);
|
|
||||||
mFindText.requestFocus();
|
|
||||||
|
|
||||||
// handleMessage() receives response message and determines initial state of softInput
|
|
||||||
GeckoAppShell.notifyObservers("TextSelection:Get", REQUEST_ID);
|
|
||||||
GeckoAppShell.notifyObservers("FindInPage:Opened", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void hide() {
|
|
||||||
if (!mInflated || getVisibility() == View.GONE) {
|
|
||||||
// There's nothing to hide yet.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always clear the Find string, primarily for privacy.
|
|
||||||
mFindText.setText("");
|
|
||||||
|
|
||||||
// Only close the IMM if its EditText is the one with focus.
|
|
||||||
if (mFindText.isFocused()) {
|
|
||||||
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the FIPB / FindHelper state.
|
|
||||||
setVisibility(GONE);
|
|
||||||
GeckoAppShell.notifyObservers("FindInPage:Closed", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private InputMethodManager getInputMethodManager(View view) {
|
|
||||||
Context context = view.getContext();
|
|
||||||
return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onDestroy() {
|
|
||||||
if (!mInflated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
|
|
||||||
"FindInPage:MatchesCountResult",
|
|
||||||
"TextSelection:Data");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onMatchesCountResult(final int total, final int current, final int limit, final String searchString) {
|
|
||||||
if (total == -1) {
|
|
||||||
updateResult(Integer.toString(limit) + "+");
|
|
||||||
} else if (total > 0) {
|
|
||||||
updateResult(Integer.toString(current) + "/" + Integer.toString(total));
|
|
||||||
} else if (TextUtils.isEmpty(searchString)) {
|
|
||||||
updateResult("");
|
|
||||||
} else {
|
|
||||||
// We display 0/0, when there were no
|
|
||||||
// matches found, or if matching has been turned off by setting
|
|
||||||
// pref accessibility.typeaheadfind.matchesCountLimit to 0.
|
|
||||||
updateResult("0/0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateResult(final String statusText) {
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mStatusText.setVisibility(statusText.isEmpty() ? View.GONE : View.VISIBLE);
|
|
||||||
mStatusText.setText(statusText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextWatcher implementation
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
sendRequestToFinderHelper("FindInPage:Find", s.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
// View.OnClickListener implementation
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
final int viewId = v.getId();
|
|
||||||
|
|
||||||
String extras = getResources().getResourceEntryName(viewId);
|
|
||||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, extras);
|
|
||||||
|
|
||||||
if (viewId == R.id.find_prev) {
|
|
||||||
sendRequestToFinderHelper("FindInPage:Prev", mFindText.getText().toString());
|
|
||||||
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewId == R.id.find_next) {
|
|
||||||
sendRequestToFinderHelper("FindInPage:Next", mFindText.getText().toString());
|
|
||||||
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewId == R.id.find_close) {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GeckoEventListener implementation
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(String event, JSONObject message) {
|
|
||||||
if (event.equals("FindInPage:MatchesCountResult")) {
|
|
||||||
onMatchesCountResult(message.optInt("total", 0),
|
|
||||||
message.optInt("current", 0),
|
|
||||||
message.optInt("limit", 0),
|
|
||||||
message.optString("searchString"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.equals("TextSelection:Data") || !REQUEST_ID.equals(message.optString("requestId"))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String text = message.optString("text");
|
|
||||||
|
|
||||||
// Populate an initial find string, virtual keyboard not required.
|
|
||||||
if (!TextUtils.isEmpty(text)) {
|
|
||||||
// Populate initial selection
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mFindText.setText(text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the virtual keyboard.
|
|
||||||
if (mFindText.hasWindowFocus()) {
|
|
||||||
getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
|
|
||||||
} else {
|
|
||||||
// showSoftInput won't work until after the window is focused.
|
|
||||||
mFindText.setOnWindowFocusChangeListener(new CustomEditText.OnWindowFocusChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onWindowFocusChanged(boolean hasFocus) {
|
|
||||||
if (!hasFocus)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mFindText.setOnWindowFocusChangeListener(null);
|
|
||||||
getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request find operation, and update matchCount results (current count and total).
|
|
||||||
*/
|
|
||||||
private void sendRequestToFinderHelper(final String request, final String searchString) {
|
|
||||||
GeckoAppShell.sendRequestToGecko(new GeckoRequest(request, searchString) {
|
|
||||||
@Override
|
|
||||||
public void onResponse(NativeJSObject nativeJSObject) {
|
|
||||||
// We don't care about the return value, because `onMatchesCountResult`
|
|
||||||
// does the heavy lifting.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(NativeJSObject error) {
|
|
||||||
// Gecko didn't respond due to state change, javascript error, etc.
|
|
||||||
Log.d(LOGTAG, "No response from Gecko on request to match string: [" +
|
|
||||||
searchString + "]");
|
|
||||||
updateResult("");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,459 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.animation.ViewHelper;
|
|
||||||
import org.mozilla.gecko.gfx.FloatSize;
|
|
||||||
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
|
|
||||||
import org.mozilla.gecko.util.GeckoEventListener;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener;
|
|
||||||
import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener.OnDismissCallback;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.PointF;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.AnimationUtils;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.RelativeLayout.LayoutParams;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public class FormAssistPopup extends RelativeLayout implements GeckoEventListener {
|
|
||||||
private final Context mContext;
|
|
||||||
private final Animation mAnimation;
|
|
||||||
|
|
||||||
private ListView mAutoCompleteList;
|
|
||||||
private RelativeLayout mValidationMessage;
|
|
||||||
private TextView mValidationMessageText;
|
|
||||||
private ImageView mValidationMessageArrow;
|
|
||||||
private ImageView mValidationMessageArrowInverted;
|
|
||||||
|
|
||||||
private double mX;
|
|
||||||
private double mY;
|
|
||||||
private double mW;
|
|
||||||
private double mH;
|
|
||||||
|
|
||||||
private enum PopupType {
|
|
||||||
AUTOCOMPLETE,
|
|
||||||
VALIDATIONMESSAGE;
|
|
||||||
}
|
|
||||||
private PopupType mPopupType;
|
|
||||||
|
|
||||||
private static final int MAX_VISIBLE_ROWS = 5;
|
|
||||||
|
|
||||||
private static int sAutoCompleteMinWidth;
|
|
||||||
private static int sAutoCompleteRowHeight;
|
|
||||||
private static int sValidationMessageHeight;
|
|
||||||
private static int sValidationTextMarginTop;
|
|
||||||
private static LayoutParams sValidationTextLayoutNormal;
|
|
||||||
private static LayoutParams sValidationTextLayoutInverted;
|
|
||||||
|
|
||||||
private static final String LOGTAG = "GeckoFormAssistPopup";
|
|
||||||
|
|
||||||
// The blocklist is so short that ArrayList is probably cheaper than HashSet.
|
|
||||||
private static final Collection<String> sInputMethodBlocklist = Arrays.asList(
|
|
||||||
InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850
|
|
||||||
InputMethods.METHOD_OPENWNN_PLUS, // bug 768108
|
|
||||||
InputMethods.METHOD_SIMEJI, // bug 768108
|
|
||||||
InputMethods.METHOD_SWYPE, // bug 755909
|
|
||||||
InputMethods.METHOD_SWYPE_BETA // bug 755909
|
|
||||||
);
|
|
||||||
|
|
||||||
public FormAssistPopup(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
mContext = context;
|
|
||||||
|
|
||||||
mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
|
|
||||||
mAnimation.setDuration(75);
|
|
||||||
|
|
||||||
setFocusable(false);
|
|
||||||
|
|
||||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
|
|
||||||
"FormAssist:AutoComplete",
|
|
||||||
"FormAssist:ValidationMessage",
|
|
||||||
"FormAssist:Hide");
|
|
||||||
}
|
|
||||||
|
|
||||||
void destroy() {
|
|
||||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
|
|
||||||
"FormAssist:AutoComplete",
|
|
||||||
"FormAssist:ValidationMessage",
|
|
||||||
"FormAssist:Hide");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(String event, JSONObject message) {
|
|
||||||
try {
|
|
||||||
if (event.equals("FormAssist:AutoComplete")) {
|
|
||||||
handleAutoCompleteMessage(message);
|
|
||||||
} else if (event.equals("FormAssist:ValidationMessage")) {
|
|
||||||
handleValidationMessage(message);
|
|
||||||
} else if (event.equals("FormAssist:Hide")) {
|
|
||||||
handleHideMessage(message);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAutoCompleteMessage(JSONObject message) throws JSONException {
|
|
||||||
final JSONArray suggestions = message.getJSONArray("suggestions");
|
|
||||||
final JSONObject rect = message.getJSONObject("rect");
|
|
||||||
final boolean isEmpty = message.getBoolean("isEmpty");
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
showAutoCompleteSuggestions(suggestions, rect, isEmpty);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleValidationMessage(JSONObject message) throws JSONException {
|
|
||||||
final String validationMessage = message.getString("validationMessage");
|
|
||||||
final JSONObject rect = message.getJSONObject("rect");
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
showValidationMessage(validationMessage, rect);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleHideMessage(JSONObject message) {
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect, boolean isEmpty) {
|
|
||||||
final String inputMethod = InputMethods.getCurrentInputMethod(mContext);
|
|
||||||
if (!isEmpty && sInputMethodBlocklist.contains(inputMethod)) {
|
|
||||||
// Don't display the form auto-complete popup after the user starts typing
|
|
||||||
// to avoid confusing somes IME. See bug 758820 and bug 632744.
|
|
||||||
hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mAutoCompleteList == null) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
|
||||||
mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null);
|
|
||||||
|
|
||||||
mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> parentView, View view, int position, long id) {
|
|
||||||
// Use the value stored with the autocomplete view, not the label text,
|
|
||||||
// since they can be different.
|
|
||||||
TextView textView = (TextView) view;
|
|
||||||
String value = (String) textView.getTag();
|
|
||||||
broadcastGeckoEvent("FormAssist:AutoComplete", value);
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a ListView-specific touch listener. ListViews are given special treatment because
|
|
||||||
// by default they handle touches for their list items... i.e. they're in charge of drawing
|
|
||||||
// the pressed state (the list selector), handling list item clicks, etc.
|
|
||||||
final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(mAutoCompleteList, new OnDismissCallback() {
|
|
||||||
@Override
|
|
||||||
public void onDismiss(ListView listView, final int position) {
|
|
||||||
// Use the value stored with the autocomplete view, not the label text,
|
|
||||||
// since they can be different.
|
|
||||||
AutoCompleteListAdapter adapter = (AutoCompleteListAdapter) listView.getAdapter();
|
|
||||||
Pair<String, String> item = adapter.getItem(position);
|
|
||||||
|
|
||||||
// Remove the item from form history.
|
|
||||||
broadcastGeckoEvent("FormAssist:Remove", item.second);
|
|
||||||
|
|
||||||
// Update the list
|
|
||||||
adapter.remove(item);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
positionAndShowPopup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mAutoCompleteList.setOnTouchListener(touchListener);
|
|
||||||
|
|
||||||
// Setting this scroll listener is required to ensure that during ListView scrolling,
|
|
||||||
// we don't look for swipes.
|
|
||||||
mAutoCompleteList.setOnScrollListener(touchListener.makeScrollListener());
|
|
||||||
|
|
||||||
// Setting this recycler listener is required to make sure animated views are reset.
|
|
||||||
mAutoCompleteList.setRecyclerListener(touchListener.makeRecyclerListener());
|
|
||||||
|
|
||||||
addView(mAutoCompleteList);
|
|
||||||
}
|
|
||||||
|
|
||||||
AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item);
|
|
||||||
adapter.populateSuggestionsList(suggestions);
|
|
||||||
mAutoCompleteList.setAdapter(adapter);
|
|
||||||
|
|
||||||
if (setGeckoPositionData(rect, true)) {
|
|
||||||
positionAndShowPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showValidationMessage(String validationMessage, JSONObject rect) {
|
|
||||||
if (mValidationMessage == null) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
|
||||||
mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null);
|
|
||||||
|
|
||||||
addView(mValidationMessage);
|
|
||||||
mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text);
|
|
||||||
|
|
||||||
sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top));
|
|
||||||
|
|
||||||
sValidationTextLayoutNormal = new LayoutParams(mValidationMessageText.getLayoutParams());
|
|
||||||
sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0);
|
|
||||||
|
|
||||||
sValidationTextLayoutInverted = new LayoutParams((ViewGroup.MarginLayoutParams) sValidationTextLayoutNormal);
|
|
||||||
sValidationTextLayoutInverted.setMargins(0, 0, 0, 0);
|
|
||||||
|
|
||||||
mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow);
|
|
||||||
mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted);
|
|
||||||
}
|
|
||||||
|
|
||||||
mValidationMessageText.setText(validationMessage);
|
|
||||||
|
|
||||||
// We need to set the text as selected for the marquee text to work.
|
|
||||||
mValidationMessageText.setSelected(true);
|
|
||||||
|
|
||||||
if (setGeckoPositionData(rect, false)) {
|
|
||||||
positionAndShowPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) {
|
|
||||||
try {
|
|
||||||
mX = rect.getDouble("x");
|
|
||||||
mY = rect.getDouble("y");
|
|
||||||
mW = rect.getDouble("w");
|
|
||||||
mH = rect.getDouble("h");
|
|
||||||
} catch (JSONException e) {
|
|
||||||
// Bail if we can't get the correct dimensions for the popup.
|
|
||||||
Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mPopupType = (isAutoComplete ?
|
|
||||||
PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void positionAndShowPopup() {
|
|
||||||
positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
|
|
||||||
// Don't show the form assist popup when using fullscreen VKB
|
|
||||||
InputMethodManager imm =
|
|
||||||
(InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
if (imm.isFullscreenMode()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide/show the appropriate popup contents
|
|
||||||
if (mAutoCompleteList != null) {
|
|
||||||
mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE);
|
|
||||||
}
|
|
||||||
if (mValidationMessage != null) {
|
|
||||||
mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sAutoCompleteMinWidth == 0) {
|
|
||||||
Resources res = mContext.getResources();
|
|
||||||
sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width));
|
|
||||||
sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height));
|
|
||||||
sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height));
|
|
||||||
}
|
|
||||||
|
|
||||||
float zoom = aMetrics.zoomFactor;
|
|
||||||
|
|
||||||
// These values correspond to the input box for which we want to
|
|
||||||
// display the FormAssistPopup.
|
|
||||||
int left = (int) (mX * zoom - aMetrics.viewportRectLeft);
|
|
||||||
int top = (int) (mY * zoom - aMetrics.viewportRectTop + GeckoAppShell.getLayerView().getSurfaceTranslation());
|
|
||||||
int width = (int) (mW * zoom);
|
|
||||||
int height = (int) (mH * zoom);
|
|
||||||
|
|
||||||
int popupWidth = LayoutParams.MATCH_PARENT;
|
|
||||||
int popupLeft = left < 0 ? 0 : left;
|
|
||||||
|
|
||||||
FloatSize viewport = aMetrics.getSize();
|
|
||||||
|
|
||||||
// For autocomplete suggestions, if the input is smaller than the screen-width,
|
|
||||||
// shrink the popup's width. Otherwise, keep it as MATCH_PARENT.
|
|
||||||
if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) {
|
|
||||||
popupWidth = left < 0 ? left + width : width;
|
|
||||||
|
|
||||||
// Ensure the popup has a minimum width.
|
|
||||||
if (popupWidth < sAutoCompleteMinWidth) {
|
|
||||||
popupWidth = sAutoCompleteMinWidth;
|
|
||||||
|
|
||||||
// Move the popup to the left if there isn't enough room for it.
|
|
||||||
if ((popupLeft + popupWidth) > viewport.width) {
|
|
||||||
popupLeft = (int) (viewport.width - popupWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int popupHeight;
|
|
||||||
if (mPopupType == PopupType.AUTOCOMPLETE) {
|
|
||||||
// Limit the amount of visible rows.
|
|
||||||
int rows = mAutoCompleteList.getAdapter().getCount();
|
|
||||||
if (rows > MAX_VISIBLE_ROWS) {
|
|
||||||
rows = MAX_VISIBLE_ROWS;
|
|
||||||
}
|
|
||||||
|
|
||||||
popupHeight = sAutoCompleteRowHeight * rows;
|
|
||||||
} else {
|
|
||||||
popupHeight = sValidationMessageHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
int popupTop = top + height;
|
|
||||||
|
|
||||||
if (mPopupType == PopupType.VALIDATIONMESSAGE) {
|
|
||||||
mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
|
|
||||||
mValidationMessageArrow.setVisibility(VISIBLE);
|
|
||||||
mValidationMessageArrowInverted.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the popup doesn't fit below the input box, shrink its height, or
|
|
||||||
// see if we can place it above the input instead.
|
|
||||||
if ((popupTop + popupHeight) > viewport.height) {
|
|
||||||
// Find where the maximum space is, and put the popup there.
|
|
||||||
if ((viewport.height - popupTop) > top) {
|
|
||||||
// Shrink the height to fit it below the input box.
|
|
||||||
popupHeight = (int) (viewport.height - popupTop);
|
|
||||||
} else {
|
|
||||||
if (popupHeight < top) {
|
|
||||||
// No shrinking needed to fit on top.
|
|
||||||
popupTop = (top - popupHeight);
|
|
||||||
} else {
|
|
||||||
// Shrink to available space on top.
|
|
||||||
popupTop = 0;
|
|
||||||
popupHeight = top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mPopupType == PopupType.VALIDATIONMESSAGE) {
|
|
||||||
mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted);
|
|
||||||
mValidationMessageArrow.setVisibility(GONE);
|
|
||||||
mValidationMessageArrowInverted.setVisibility(VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LayoutParams layoutParams = new LayoutParams(popupWidth, popupHeight);
|
|
||||||
layoutParams.setMargins(popupLeft, popupTop, 0, 0);
|
|
||||||
setLayoutParams(layoutParams);
|
|
||||||
requestLayout();
|
|
||||||
|
|
||||||
if (!isShown()) {
|
|
||||||
setVisibility(VISIBLE);
|
|
||||||
startAnimation(mAnimation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void hide() {
|
|
||||||
if (isShown()) {
|
|
||||||
setVisibility(GONE);
|
|
||||||
broadcastGeckoEvent("FormAssist:Hidden", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onTranslationChanged() {
|
|
||||||
ThreadUtils.assertOnUiThread();
|
|
||||||
if (!isShown()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
positionAndShowPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMetricsChanged(final ImmutableViewportMetrics aMetrics) {
|
|
||||||
if (!isShown()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
positionAndShowPopup(aMetrics);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void broadcastGeckoEvent(String eventName, String eventData) {
|
|
||||||
GeckoAppShell.notifyObservers(eventName, eventData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> {
|
|
||||||
private final LayoutInflater mInflater;
|
|
||||||
private final int mTextViewResourceId;
|
|
||||||
|
|
||||||
public AutoCompleteListAdapter(Context context, int textViewResourceId) {
|
|
||||||
super(context, textViewResourceId);
|
|
||||||
|
|
||||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
mTextViewResourceId = textViewResourceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method takes an array of autocomplete suggestions with label/value properties
|
|
||||||
// and adds label/value Pair objects to the array that backs the adapter.
|
|
||||||
public void populateSuggestionsList(JSONArray suggestions) {
|
|
||||||
try {
|
|
||||||
for (int i = 0; i < suggestions.length(); i++) {
|
|
||||||
JSONObject suggestion = suggestions.getJSONObject(i);
|
|
||||||
String label = suggestion.getString("label");
|
|
||||||
String value = suggestion.getString("value");
|
|
||||||
add(new Pair<String, String>(label, value));
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(LOGTAG, "JSONException", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
if (convertView == null) {
|
|
||||||
convertView = mInflater.inflate(mTextViewResourceId, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair<String, String> item = getItem(position);
|
|
||||||
TextView itemView = (TextView) convertView;
|
|
||||||
|
|
||||||
// Set the text with the suggestion label
|
|
||||||
itemView.setText(item.first);
|
|
||||||
|
|
||||||
// Set a tag with the suggestion value
|
|
||||||
itemView.setTag(item.second);
|
|
||||||
|
|
||||||
return convertView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
|
|
||||||
public abstract class GeckoActivity extends AppCompatActivity implements GeckoActivityStatus {
|
|
||||||
// has this activity recently started another Gecko activity?
|
|
||||||
private boolean mGeckoActivityOpened;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display any resources that show strings or encompass locale-specific
|
|
||||||
* representations.
|
|
||||||
*
|
|
||||||
* onLocaleReady must always be called on the UI thread.
|
|
||||||
*/
|
|
||||||
public void onLocaleReady(final String locale) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
|
|
||||||
if (getApplication() instanceof GeckoApplication) {
|
|
||||||
((GeckoApplication) getApplication()).onActivityPause(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
if (getApplication() instanceof GeckoApplication) {
|
|
||||||
((GeckoApplication) getApplication()).onActivityResume(this);
|
|
||||||
mGeckoActivityOpened = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(android.os.Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
if (AppConstants.MOZ_ANDROID_ANR_REPORTER) {
|
|
||||||
ANRReporter.register(getApplicationContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
if (AppConstants.MOZ_ANDROID_ANR_REPORTER) {
|
|
||||||
ANRReporter.unregister();
|
|
||||||
}
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startActivity(Intent intent) {
|
|
||||||
mGeckoActivityOpened = checkIfGeckoActivity(intent);
|
|
||||||
super.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startActivityForResult(Intent intent, int request) {
|
|
||||||
mGeckoActivityOpened = checkIfGeckoActivity(intent);
|
|
||||||
super.startActivityForResult(intent, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean checkIfGeckoActivity(Intent intent) {
|
|
||||||
// Whenever we call our own activity, the component and its package name is set.
|
|
||||||
// If we call an activity from another package, or an open intent (leaving android to resolve)
|
|
||||||
// component has a different package name or it is null.
|
|
||||||
ComponentName component = intent.getComponent();
|
|
||||||
return (component != null &&
|
|
||||||
AppConstants.ANDROID_PACKAGE_NAME.equals(component.getPackageName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isGeckoActivityOpened() {
|
|
||||||
return mGeckoActivityOpened;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isApplicationInBackground() {
|
|
||||||
return ((GeckoApplication) getApplication()).isApplicationInBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLowMemory() {
|
|
||||||
MemoryMonitor.getInstance().onLowMemory();
|
|
||||||
super.onLowMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrimMemory(int level) {
|
|
||||||
MemoryMonitor.getInstance().onTrimMemory(level);
|
|
||||||
super.onTrimMemory(level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
public interface GeckoActivityStatus {
|
|
||||||
public boolean isGeckoActivityOpened();
|
|
||||||
public boolean isFinishing(); // typically from android.app.Activity
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,314 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.squareup.leakcanary.LeakCanary;
|
|
||||||
import com.squareup.leakcanary.RefWatcher;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.db.BrowserContract;
|
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
|
||||||
import org.mozilla.gecko.db.LocalBrowserDB;
|
|
||||||
import org.mozilla.gecko.distribution.Distribution;
|
|
||||||
import org.mozilla.gecko.dlc.DownloadContentService;
|
|
||||||
import org.mozilla.gecko.home.HomePanelsManager;
|
|
||||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
|
||||||
import org.mozilla.gecko.mdns.MulticastDNSManager;
|
|
||||||
import org.mozilla.gecko.media.AudioFocusAgent;
|
|
||||||
import org.mozilla.gecko.notifications.NotificationClient;
|
|
||||||
import org.mozilla.gecko.notifications.NotificationHelper;
|
|
||||||
import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
|
|
||||||
import org.mozilla.gecko.util.BundleEventListener;
|
|
||||||
import org.mozilla.gecko.util.Clipboard;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
import org.mozilla.gecko.util.HardwareUtils;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public class GeckoApplication extends Application
|
|
||||||
implements ContextGetter {
|
|
||||||
private static final String LOG_TAG = "GeckoApplication";
|
|
||||||
|
|
||||||
private static volatile GeckoApplication instance;
|
|
||||||
|
|
||||||
private boolean mInBackground;
|
|
||||||
private boolean mPausedGecko;
|
|
||||||
|
|
||||||
private LightweightTheme mLightweightTheme;
|
|
||||||
|
|
||||||
private RefWatcher mRefWatcher;
|
|
||||||
|
|
||||||
public GeckoApplication() {
|
|
||||||
super();
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GeckoApplication get() {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RefWatcher getRefWatcher(Context context) {
|
|
||||||
GeckoApplication app = (GeckoApplication) context.getApplicationContext();
|
|
||||||
return app.mRefWatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void watchReference(Context context, Object object) {
|
|
||||||
if (context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRefWatcher(context).watch(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Context getContext() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences getSharedPreferences() {
|
|
||||||
return GeckoSharedPrefs.forApp(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need to do locale work here, because we need to intercept
|
|
||||||
* each hit to onConfigurationChanged.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(Configuration config) {
|
|
||||||
Log.d(LOG_TAG, "onConfigurationChanged: " + config.locale +
|
|
||||||
", background: " + mInBackground);
|
|
||||||
|
|
||||||
// Do nothing if we're in the background. It'll simply cause a loop
|
|
||||||
// (Bug 936756 Comment 11), and it's not necessary.
|
|
||||||
if (mInBackground) {
|
|
||||||
super.onConfigurationChanged(config);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, correct the locale. This catches some cases that GeckoApp
|
|
||||||
// doesn't get a chance to.
|
|
||||||
try {
|
|
||||||
BrowserLocaleManager.getInstance().correctLocale(this, getResources(), config);
|
|
||||||
} catch (IllegalStateException ex) {
|
|
||||||
// GeckoApp hasn't started, so we have no ContextGetter in BrowserLocaleManager.
|
|
||||||
Log.w(LOG_TAG, "Couldn't correct locale.", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onConfigurationChanged(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onActivityPause(GeckoActivityStatus activity) {
|
|
||||||
mInBackground = true;
|
|
||||||
|
|
||||||
if ((activity.isFinishing() == false) &&
|
|
||||||
(activity.isGeckoActivityOpened() == false)) {
|
|
||||||
// Notify Gecko that we are pausing; the cache service will be
|
|
||||||
// shutdown, closing the disk cache cleanly. If the android
|
|
||||||
// low memory killer subsequently kills us, the disk cache will
|
|
||||||
// be left in a consistent state, avoiding costly cleanup and
|
|
||||||
// re-creation.
|
|
||||||
GeckoThread.onPause();
|
|
||||||
mPausedGecko = true;
|
|
||||||
|
|
||||||
final BrowserDB db = BrowserDB.from(this);
|
|
||||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
db.expireHistory(getContentResolver(), BrowserContract.ExpirePriority.NORMAL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
GeckoNetworkManager.getInstance().stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onActivityResume(GeckoActivityStatus activity) {
|
|
||||||
if (mPausedGecko) {
|
|
||||||
GeckoThread.onResume();
|
|
||||||
mPausedGecko = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
GeckoBatteryManager.getInstance().start(this);
|
|
||||||
GeckoNetworkManager.getInstance().start(this);
|
|
||||||
|
|
||||||
mInBackground = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
AppConstants.maybeInstallMultiDex(base);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
Log.i(LOG_TAG, "zerdatime " + SystemClock.uptimeMillis() + " - Fennec application start");
|
|
||||||
|
|
||||||
mRefWatcher = LeakCanary.install(this);
|
|
||||||
|
|
||||||
final Context context = getApplicationContext();
|
|
||||||
GeckoAppShell.setApplicationContext(context);
|
|
||||||
HardwareUtils.init(context);
|
|
||||||
Clipboard.init(context);
|
|
||||||
FilePicker.init(context);
|
|
||||||
DownloadsIntegration.init();
|
|
||||||
HomePanelsManager.getInstance().init(context);
|
|
||||||
|
|
||||||
GlobalPageMetadata.getInstance().init();
|
|
||||||
|
|
||||||
// We need to set the notification client before launching Gecko, since Gecko could start
|
|
||||||
// sending notifications immediately after startup, which we don't want to lose/crash on.
|
|
||||||
GeckoAppShell.setNotificationListener(new NotificationClient(context));
|
|
||||||
// This getInstance call will force initialization of the NotificationHelper, but does nothing with the result
|
|
||||||
NotificationHelper.getInstance(context).init();
|
|
||||||
|
|
||||||
MulticastDNSManager.getInstance(context).init();
|
|
||||||
|
|
||||||
GeckoService.register();
|
|
||||||
|
|
||||||
EventDispatcher.getInstance().registerBackgroundThreadListener(new EventListener(),
|
|
||||||
"Profile:Create");
|
|
||||||
|
|
||||||
super.onCreate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onDelayedStartup() {
|
|
||||||
if (AppConstants.MOZ_ANDROID_GCM) {
|
|
||||||
// TODO: only run in main process.
|
|
||||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// It's fine to throw GCM initialization onto a background thread; the registration process requires
|
|
||||||
// network access, so is naturally asynchronous. This, of course, races against Gecko page load of
|
|
||||||
// content requiring GCM-backed services, like Web Push. There's nothing to be done here.
|
|
||||||
try {
|
|
||||||
final Class<?> clazz = Class.forName("org.mozilla.gecko.push.PushService");
|
|
||||||
final Method onCreate = clazz.getMethod("onCreate", Context.class);
|
|
||||||
onCreate.invoke(null, getApplicationContext()); // Method is static.
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
|
|
||||||
DownloadContentService.startStudy(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
GeckoAccessibility.setAccessibilityManagerListeners(this);
|
|
||||||
|
|
||||||
AudioFocusAgent.getInstance().attachToContext(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class EventListener implements BundleEventListener
|
|
||||||
{
|
|
||||||
private void onProfileCreate(final String name, final String path) {
|
|
||||||
// Add everything when we're done loading the distribution.
|
|
||||||
final Context context = GeckoApplication.this;
|
|
||||||
final GeckoProfile profile = GeckoProfile.get(context, name);
|
|
||||||
final Distribution distribution = Distribution.getInstance(context);
|
|
||||||
|
|
||||||
distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
|
|
||||||
@Override
|
|
||||||
public void distributionNotFound() {
|
|
||||||
this.distributionFound(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void distributionFound(final Distribution distribution) {
|
|
||||||
Log.d(LOG_TAG, "Running post-distribution task: bookmarks.");
|
|
||||||
// Because we are running in the background, we want to synchronize on the
|
|
||||||
// GeckoProfile instance so that we don't race with main thread operations
|
|
||||||
// such as locking/unlocking/removing the profile.
|
|
||||||
synchronized (profile.getLock()) {
|
|
||||||
distributionFoundLocked(distribution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void distributionArrivedLate(final Distribution distribution) {
|
|
||||||
Log.d(LOG_TAG, "Running late distribution task: bookmarks.");
|
|
||||||
// Recover as best we can.
|
|
||||||
synchronized (profile.getLock()) {
|
|
||||||
distributionArrivedLateLocked(distribution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void distributionFoundLocked(final Distribution distribution) {
|
|
||||||
// Skip initialization if the profile directory has been removed.
|
|
||||||
if (!(new File(path)).exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ContentResolver cr = context.getContentResolver();
|
|
||||||
final LocalBrowserDB db = new LocalBrowserDB(profile.getName());
|
|
||||||
|
|
||||||
// We pass the number of added bookmarks to ensure that the
|
|
||||||
// indices of the distribution and default bookmarks are
|
|
||||||
// contiguous. Because there are always at least as many
|
|
||||||
// bookmarks as there are favicons, we can also guarantee that
|
|
||||||
// the favicon IDs won't overlap.
|
|
||||||
final int offset = distribution == null ? 0 :
|
|
||||||
db.addDistributionBookmarks(cr, distribution, 0);
|
|
||||||
db.addDefaultBookmarks(context, cr, offset);
|
|
||||||
|
|
||||||
Log.d(LOG_TAG, "Running post-distribution task: android preferences.");
|
|
||||||
DistroSharedPrefsImport.importPreferences(context, distribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void distributionArrivedLateLocked(final Distribution distribution) {
|
|
||||||
// Skip initialization if the profile directory has been removed.
|
|
||||||
if (!(new File(path)).exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ContentResolver cr = context.getContentResolver();
|
|
||||||
final LocalBrowserDB db = new LocalBrowserDB(profile.getName());
|
|
||||||
|
|
||||||
// We assume we've been called very soon after startup, and so our offset
|
|
||||||
// into "Mobile Bookmarks" is the number of bookmarks in the DB.
|
|
||||||
final int offset = db.getCount(cr, "bookmarks");
|
|
||||||
db.addDistributionBookmarks(cr, distribution, offset);
|
|
||||||
|
|
||||||
Log.d(LOG_TAG, "Running late distribution task: android preferences.");
|
|
||||||
DistroSharedPrefsImport.importPreferences(context, distribution);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // BundleEventListener
|
|
||||||
public void handleMessage(final String event, final Bundle message,
|
|
||||||
final EventCallback callback) {
|
|
||||||
if ("Profile:Create".equals(event)) {
|
|
||||||
onProfileCreate(message.getCharSequence("name").toString(),
|
|
||||||
message.getCharSequence("path").toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isApplicationInBackground() {
|
|
||||||
return mInBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightweightTheme getLightweightTheme() {
|
|
||||||
return mLightweightTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void prepareLightweightTheme() {
|
|
||||||
mLightweightTheme = new LightweightTheme(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.util.SparseArray;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
||||||
|
|
||||||
import java.lang.Thread;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class GeckoJavaSampler {
|
|
||||||
private static final String LOGTAG = "JavaSampler";
|
|
||||||
private static Thread sSamplingThread;
|
|
||||||
private static SamplingThread sSamplingRunnable;
|
|
||||||
private static Thread sMainThread;
|
|
||||||
|
|
||||||
// Use the same timer primitive as the profiler
|
|
||||||
// to get a perfect sample syncing.
|
|
||||||
@WrapForJNI
|
|
||||||
private static native double getProfilerTime();
|
|
||||||
|
|
||||||
private static class Sample {
|
|
||||||
public Frame[] mFrames;
|
|
||||||
public double mTime;
|
|
||||||
public long mJavaTime; // non-zero if Android system time is used
|
|
||||||
public Sample(StackTraceElement[] aStack) {
|
|
||||||
mFrames = new Frame[aStack.length];
|
|
||||||
if (GeckoThread.isStateAtLeast(GeckoThread.State.LIBS_READY)) {
|
|
||||||
mTime = getProfilerTime();
|
|
||||||
}
|
|
||||||
if (mTime == 0.0d) {
|
|
||||||
// getProfilerTime is not available yet; either libs are not loaded,
|
|
||||||
// or profiling hasn't started on the Gecko side yet
|
|
||||||
mJavaTime = SystemClock.elapsedRealtime();
|
|
||||||
}
|
|
||||||
for (int i = 0; i < aStack.length; i++) {
|
|
||||||
mFrames[aStack.length - 1 - i] = new Frame();
|
|
||||||
mFrames[aStack.length - 1 - i].fileName = aStack[i].getFileName();
|
|
||||||
mFrames[aStack.length - 1 - i].lineNo = aStack[i].getLineNumber();
|
|
||||||
mFrames[aStack.length - 1 - i].methodName = aStack[i].getMethodName();
|
|
||||||
mFrames[aStack.length - 1 - i].className = aStack[i].getClassName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private static class Frame {
|
|
||||||
public String fileName;
|
|
||||||
public int lineNo;
|
|
||||||
public String methodName;
|
|
||||||
public String className;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SamplingThread implements Runnable {
|
|
||||||
private final int mInterval;
|
|
||||||
private final int mSampleCount;
|
|
||||||
|
|
||||||
private boolean mPauseSampler;
|
|
||||||
private boolean mStopSampler;
|
|
||||||
|
|
||||||
private final SparseArray<Sample[]> mSamples = new SparseArray<Sample[]>();
|
|
||||||
private int mSamplePos;
|
|
||||||
|
|
||||||
public SamplingThread(final int aInterval, final int aSampleCount) {
|
|
||||||
// If we sample faster then 10ms we get to many missed samples
|
|
||||||
mInterval = Math.max(10, aInterval);
|
|
||||||
mSampleCount = aSampleCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
synchronized (GeckoJavaSampler.class) {
|
|
||||||
mSamples.put(0, new Sample[mSampleCount]);
|
|
||||||
mSamplePos = 0;
|
|
||||||
|
|
||||||
// Find the main thread
|
|
||||||
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
|
|
||||||
for (Thread t : threadSet) {
|
|
||||||
if (t.getName().compareToIgnoreCase("main") == 0) {
|
|
||||||
sMainThread = t;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sMainThread == null) {
|
|
||||||
Log.e(LOGTAG, "Main thread not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(mInterval);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
synchronized (GeckoJavaSampler.class) {
|
|
||||||
if (!mPauseSampler) {
|
|
||||||
StackTraceElement[] bt = sMainThread.getStackTrace();
|
|
||||||
mSamples.get(0)[mSamplePos] = new Sample(bt);
|
|
||||||
mSamplePos = (mSamplePos + 1) % mSamples.get(0).length;
|
|
||||||
}
|
|
||||||
if (mStopSampler) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Sample getSample(int aThreadId, int aSampleId) {
|
|
||||||
if (aThreadId < mSamples.size() && aSampleId < mSamples.get(aThreadId).length &&
|
|
||||||
mSamples.get(aThreadId)[aSampleId] != null) {
|
|
||||||
int startPos = 0;
|
|
||||||
if (mSamples.get(aThreadId)[mSamplePos] != null) {
|
|
||||||
startPos = mSamplePos;
|
|
||||||
}
|
|
||||||
int readPos = (startPos + aSampleId) % mSamples.get(aThreadId).length;
|
|
||||||
return mSamples.get(aThreadId)[readPos];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@WrapForJNI
|
|
||||||
public synchronized static String getThreadName(int aThreadId) {
|
|
||||||
if (aThreadId == 0 && sMainThread != null) {
|
|
||||||
return sMainThread.getName();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized static Sample getSample(int aThreadId, int aSampleId) {
|
|
||||||
return sSamplingRunnable.getSample(aThreadId, aSampleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI
|
|
||||||
public synchronized static double getSampleTime(int aThreadId, int aSampleId) {
|
|
||||||
Sample sample = getSample(aThreadId, aSampleId);
|
|
||||||
if (sample != null) {
|
|
||||||
if (sample.mJavaTime != 0) {
|
|
||||||
return (sample.mJavaTime -
|
|
||||||
SystemClock.elapsedRealtime()) + getProfilerTime();
|
|
||||||
}
|
|
||||||
System.out.println("Sample: " + sample.mTime);
|
|
||||||
return sample.mTime;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI
|
|
||||||
public synchronized static String getFrameName(int aThreadId, int aSampleId, int aFrameId) {
|
|
||||||
Sample sample = getSample(aThreadId, aSampleId);
|
|
||||||
if (sample != null && aFrameId < sample.mFrames.length) {
|
|
||||||
Frame frame = sample.mFrames[aFrameId];
|
|
||||||
if (frame == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return frame.className + "." + frame.methodName + "()";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI
|
|
||||||
public static void start(int aInterval, int aSamples) {
|
|
||||||
synchronized (GeckoJavaSampler.class) {
|
|
||||||
if (sSamplingRunnable != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sSamplingRunnable = new SamplingThread(aInterval, aSamples);
|
|
||||||
sSamplingThread = new Thread(sSamplingRunnable, "Java Sampler");
|
|
||||||
sSamplingThread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI
|
|
||||||
public static void pause() {
|
|
||||||
synchronized (GeckoJavaSampler.class) {
|
|
||||||
sSamplingRunnable.mPauseSampler = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI
|
|
||||||
public static void unpause() {
|
|
||||||
synchronized (GeckoJavaSampler.class) {
|
|
||||||
sSamplingRunnable.mPauseSampler = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI
|
|
||||||
public static void stop() {
|
|
||||||
synchronized (GeckoJavaSampler.class) {
|
|
||||||
if (sSamplingThread == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sSamplingRunnable.mStopSampler = true;
|
|
||||||
try {
|
|
||||||
sSamplingThread.join();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
sSamplingThread = null;
|
|
||||||
sSamplingRunnable = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for MediaRouter types supported by Android, such as Chromecast, Miracast, etc.
|
|
||||||
*/
|
|
||||||
interface GeckoMediaPlayer {
|
|
||||||
/**
|
|
||||||
* Can return null.
|
|
||||||
*/
|
|
||||||
JSONObject toJSON();
|
|
||||||
void load(String title, String url, String type, EventCallback callback);
|
|
||||||
void play(EventCallback callback);
|
|
||||||
void pause(EventCallback callback);
|
|
||||||
void stop(EventCallback callback);
|
|
||||||
void start(EventCallback callback);
|
|
||||||
void end(EventCallback callback);
|
|
||||||
void mirror(EventCallback callback);
|
|
||||||
void message(String message, EventCallback callback);
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
public class GeckoMessageReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
final String action = intent.getAction();
|
|
||||||
if (GeckoApp.ACTION_INIT_PW.equals(action)) {
|
|
||||||
GeckoAppShell.notifyObservers("Passwords:Init", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for MediaRouter types supported by Android to use for
|
|
||||||
* Presentation API, such as Chromecast, Miracast, etc.
|
|
||||||
*/
|
|
||||||
interface GeckoPresentationDisplay {
|
|
||||||
/**
|
|
||||||
* Can return null.
|
|
||||||
*/
|
|
||||||
JSONObject toJSON();
|
|
||||||
void start(EventCallback callback);
|
|
||||||
void stop(EventCallback callback);
|
|
||||||
}
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
|
|
||||||
import org.mozilla.gecko.db.BrowserContract;
|
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.UriMatcher;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is not a per-profile provider. This provider allows read-only,
|
|
||||||
* restricted access to certain attributes of Fennec profiles.
|
|
||||||
*/
|
|
||||||
public class GeckoProfilesProvider extends ContentProvider {
|
|
||||||
private static final String LOG_TAG = "GeckoProfilesProvider";
|
|
||||||
|
|
||||||
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
|
|
||||||
|
|
||||||
private static final int PROFILES = 100;
|
|
||||||
private static final int PROFILES_NAME = 101;
|
|
||||||
private static final int PROFILES_DEFAULT = 200;
|
|
||||||
|
|
||||||
private static final String[] DEFAULT_ARGS = {
|
|
||||||
BrowserContract.Profiles.NAME,
|
|
||||||
BrowserContract.Profiles.PATH,
|
|
||||||
};
|
|
||||||
|
|
||||||
static {
|
|
||||||
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles", PROFILES);
|
|
||||||
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles/*", PROFILES_NAME);
|
|
||||||
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "default", PROFILES_DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType(Uri uri) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
// Successfully loaded.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] profileValues(final String name, final String path, int len, int nameIndex, int pathIndex) {
|
|
||||||
final String[] values = new String[len];
|
|
||||||
if (nameIndex >= 0) {
|
|
||||||
values[nameIndex] = name;
|
|
||||||
}
|
|
||||||
if (pathIndex >= 0) {
|
|
||||||
values[pathIndex] = path;
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void addRowForProfile(final MatrixCursor cursor, final int len, final int nameIndex, final int pathIndex, final String name, final String path) {
|
|
||||||
if (path == null || name == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor.addRow(profileValues(name, path, len, nameIndex, pathIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Cursor getCursorForProfiles(final String[] args, Map<String, String> profiles) {
|
|
||||||
// Compute the projection.
|
|
||||||
int nameIndex = -1;
|
|
||||||
int pathIndex = -1;
|
|
||||||
for (int i = 0; i < args.length; ++i) {
|
|
||||||
if (BrowserContract.Profiles.NAME.equals(args[i])) {
|
|
||||||
nameIndex = i;
|
|
||||||
} else if (BrowserContract.Profiles.PATH.equals(args[i])) {
|
|
||||||
pathIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final MatrixCursor cursor = new MatrixCursor(args);
|
|
||||||
for (Entry<String, String> entry : profiles.entrySet()) {
|
|
||||||
addRowForProfile(cursor, args.length, nameIndex, pathIndex, entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor query(Uri uri, String[] projection, String selection,
|
|
||||||
String[] selectionArgs, String sortOrder) {
|
|
||||||
|
|
||||||
final String[] args = (projection == null) ? DEFAULT_ARGS : projection;
|
|
||||||
|
|
||||||
final File mozillaDir;
|
|
||||||
try {
|
|
||||||
mozillaDir = GeckoProfileDirectories.getMozillaDirectory(getContext());
|
|
||||||
} catch (NoMozillaDirectoryException e) {
|
|
||||||
Log.d(LOG_TAG, "No Mozilla directory; cannot query for profiles. Assuming there are none.");
|
|
||||||
return new MatrixCursor(projection);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, String> matchingProfiles;
|
|
||||||
|
|
||||||
final int match = URI_MATCHER.match(uri);
|
|
||||||
switch (match) {
|
|
||||||
case PROFILES:
|
|
||||||
// Return all profiles.
|
|
||||||
matchingProfiles = GeckoProfileDirectories.getAllProfiles(mozillaDir);
|
|
||||||
break;
|
|
||||||
case PROFILES_NAME:
|
|
||||||
// Return data about the specified profile.
|
|
||||||
final String name = uri.getLastPathSegment();
|
|
||||||
matchingProfiles = GeckoProfileDirectories.getProfilesNamed(mozillaDir,
|
|
||||||
name);
|
|
||||||
break;
|
|
||||||
case PROFILES_DEFAULT:
|
|
||||||
matchingProfiles = GeckoProfileDirectories.getDefaultProfile(mozillaDir);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException("Unknown query URI " + uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getCursorForProfiles(args, matchingProfiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri insert(Uri uri, ContentValues values) {
|
|
||||||
throw new IllegalStateException("Inserts not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
|
||||||
throw new IllegalStateException("Deletes not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int update(Uri uri, ContentValues values, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
throw new IllegalStateException("Updates not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,236 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.util.NativeEventListener;
|
|
||||||
import org.mozilla.gecko.util.NativeJSObject;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
|
|
||||||
public class GeckoService extends Service {
|
|
||||||
|
|
||||||
private static final String LOGTAG = "GeckoService";
|
|
||||||
private static final boolean DEBUG = false;
|
|
||||||
|
|
||||||
private static final String INTENT_PROFILE_NAME = "org.mozilla.gecko.intent.PROFILE_NAME";
|
|
||||||
private static final String INTENT_PROFILE_DIR = "org.mozilla.gecko.intent.PROFILE_DIR";
|
|
||||||
|
|
||||||
private static final String INTENT_ACTION_UPDATE_ADDONS = "update-addons";
|
|
||||||
private static final String INTENT_ACTION_CREATE_SERVICES = "create-services";
|
|
||||||
|
|
||||||
private static final String INTENT_SERVICE_CATEGORY = "category";
|
|
||||||
private static final String INTENT_SERVICE_DATA = "data";
|
|
||||||
|
|
||||||
private static class EventListener implements NativeEventListener {
|
|
||||||
@Override // NativeEventListener
|
|
||||||
public void handleMessage(final String event,
|
|
||||||
final NativeJSObject message,
|
|
||||||
final EventCallback callback) {
|
|
||||||
final Context context = GeckoAppShell.getApplicationContext();
|
|
||||||
switch (event) {
|
|
||||||
case "Gecko:ScheduleRun":
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "Scheduling " + message.getString("action") +
|
|
||||||
" @ " + message.getInt("interval") + "ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
final Intent intent = getIntentForAction(context, message.getString("action"));
|
|
||||||
final PendingIntent pendingIntent = PendingIntent.getService(
|
|
||||||
context, /* requestCode */ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
|
||||||
|
|
||||||
final AlarmManager am = (AlarmManager)
|
|
||||||
context.getSystemService(Context.ALARM_SERVICE);
|
|
||||||
// Cancel any previous alarm and schedule a new one.
|
|
||||||
am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
|
|
||||||
message.getInt("trigger"),
|
|
||||||
message.getInt("interval"),
|
|
||||||
pendingIntent);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final EventListener EVENT_LISTENER = new EventListener();
|
|
||||||
|
|
||||||
public static void register() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "Registered listener");
|
|
||||||
}
|
|
||||||
EventDispatcher.getInstance().registerGeckoThreadListener(EVENT_LISTENER,
|
|
||||||
"Gecko:ScheduleRun");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unregister() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "Unregistered listener");
|
|
||||||
}
|
|
||||||
EventDispatcher.getInstance().unregisterGeckoThreadListener(EVENT_LISTENER,
|
|
||||||
"Gecko:ScheduleRun");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // Service
|
|
||||||
public void onCreate() {
|
|
||||||
GeckoAppShell.ensureCrashHandling();
|
|
||||||
GeckoThread.onResume();
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "Created");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // Service
|
|
||||||
public void onDestroy() {
|
|
||||||
GeckoThread.onPause();
|
|
||||||
|
|
||||||
// We want to block here if we can, so we don't get killed when Gecko is in the
|
|
||||||
// middle of handling onPause().
|
|
||||||
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
|
|
||||||
GeckoThread.waitOnGecko();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "Destroyed");
|
|
||||||
}
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Intent getIntentForAction(final Context context, final String action) {
|
|
||||||
final Intent intent = new Intent(action, /* uri */ null, context, GeckoService.class);
|
|
||||||
final GeckoProfile profile = GeckoThread.getActiveProfile();
|
|
||||||
if (profile != null) {
|
|
||||||
setIntentProfile(intent, profile.getName(), profile.getDir().getAbsolutePath());
|
|
||||||
}
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Intent getIntentToCreateServices(final Context context, final String category, final String data) {
|
|
||||||
final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES);
|
|
||||||
intent.putExtra(INTENT_SERVICE_CATEGORY, category);
|
|
||||||
intent.putExtra(INTENT_SERVICE_DATA, data);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Intent getIntentToCreateServices(final Context context, final String category) {
|
|
||||||
return getIntentToCreateServices(context, category, /* data */ null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setIntentProfile(final Intent intent, final String profileName,
|
|
||||||
final String profileDir) {
|
|
||||||
intent.putExtra(INTENT_PROFILE_NAME, profileName);
|
|
||||||
intent.putExtra(INTENT_PROFILE_DIR, profileDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int handleIntent(final Intent intent, final int startId) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(LOGTAG, "Handling " + intent.getAction());
|
|
||||||
}
|
|
||||||
|
|
||||||
final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
|
|
||||||
final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
|
|
||||||
|
|
||||||
if (profileName == null) {
|
|
||||||
throw new IllegalArgumentException("Intent must specify profile.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
|
|
||||||
profileDir != null ? new File(profileDir) : null)) {
|
|
||||||
Log.w(LOGTAG, "Ignoring due to profile mismatch: " +
|
|
||||||
profileName + " [" + profileDir + ']');
|
|
||||||
|
|
||||||
final GeckoProfile profile = GeckoThread.getActiveProfile();
|
|
||||||
if (profile != null) {
|
|
||||||
Log.w(LOGTAG, "Current profile is " + profile.getName() +
|
|
||||||
" [" + profile.getDir().getAbsolutePath() + ']');
|
|
||||||
}
|
|
||||||
stopSelf(startId);
|
|
||||||
return Service.START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
GeckoThread.launch();
|
|
||||||
|
|
||||||
switch (intent.getAction()) {
|
|
||||||
case INTENT_ACTION_UPDATE_ADDONS:
|
|
||||||
// Run the add-on update service. Because the service is automatically invoked
|
|
||||||
// when loading Gecko, we don't have to do anything else here.
|
|
||||||
break;
|
|
||||||
|
|
||||||
case INTENT_ACTION_CREATE_SERVICES:
|
|
||||||
final String category = intent.getStringExtra(INTENT_SERVICE_CATEGORY);
|
|
||||||
final String data = intent.getStringExtra(INTENT_SERVICE_DATA);
|
|
||||||
|
|
||||||
if (category == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
GeckoThread.createServices(category, data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Log.w(LOGTAG, "Unknown request: " + intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
stopSelf(startId);
|
|
||||||
return Service.START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // Service
|
|
||||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
||||||
if (intent == null) {
|
|
||||||
return Service.START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return handleIntent(intent, startId);
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
Log.e(LOGTAG, "Cannot handle intent: " + intent, e);
|
|
||||||
return Service.START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // Service
|
|
||||||
public IBinder onBind(final Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void startGecko(final GeckoProfile profile, final String args, final Context context) {
|
|
||||||
if (GeckoThread.isLaunched()) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.v(LOGTAG, "already launched");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Handler handler = new Handler(Looper.getMainLooper());
|
|
||||||
handler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
GeckoAppShell.ensureCrashHandling();
|
|
||||||
GeckoAppShell.setApplicationContext(context);
|
|
||||||
GeckoThread.onResume();
|
|
||||||
|
|
||||||
GeckoThread.init(profile, args, null, false);
|
|
||||||
GeckoThread.launch();
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.v(LOGTAG, "warmed up (launched)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.updater.UpdateServiceHelper;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
public class GeckoUpdateReceiver extends BroadcastReceiver
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT.equals(intent.getAction())) {
|
|
||||||
String result = intent.getStringExtra("result");
|
|
||||||
if (GeckoAppShell.getGeckoInterface() != null && result != null) {
|
|
||||||
GeckoAppShell.getGeckoInterface().notifyCheckUpdateResult(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
|
||||||
import org.mozilla.gecko.reader.ReaderModeUtils;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
class GlobalHistory {
|
|
||||||
private static final String LOGTAG = "GeckoGlobalHistory";
|
|
||||||
|
|
||||||
public static final String EVENT_URI_AVAILABLE_IN_HISTORY = "URI_INSERTED_TO_HISTORY";
|
|
||||||
public static final String EVENT_PARAM_URI = "uri";
|
|
||||||
|
|
||||||
private static final String TELEMETRY_HISTOGRAM_ADD = "FENNEC_GLOBALHISTORY_ADD_MS";
|
|
||||||
private static final String TELEMETRY_HISTOGRAM_UPDATE = "FENNEC_GLOBALHISTORY_UPDATE_MS";
|
|
||||||
private static final String TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK = "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS";
|
|
||||||
|
|
||||||
private static final GlobalHistory sInstance = new GlobalHistory();
|
|
||||||
|
|
||||||
static GlobalHistory getInstance() {
|
|
||||||
return sInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is the delay between receiving a URI check request and processing it.
|
|
||||||
// this allows batching together multiple requests and processing them together,
|
|
||||||
// which is more efficient.
|
|
||||||
private static final long BATCHING_DELAY_MS = 100;
|
|
||||||
|
|
||||||
private final Handler mHandler; // a background thread on which we can process requests
|
|
||||||
|
|
||||||
// Note: These fields are accessed through the NotificationRunnable inner class.
|
|
||||||
final Queue<String> mPendingUris; // URIs that need to be checked
|
|
||||||
SoftReference<Set<String>> mVisitedCache; // cache of the visited URI list
|
|
||||||
boolean mProcessing; // = false // whether or not the runnable is queued/working
|
|
||||||
|
|
||||||
private class NotifierRunnable implements Runnable {
|
|
||||||
private final ContentResolver mContentResolver;
|
|
||||||
private final BrowserDB mDB;
|
|
||||||
|
|
||||||
public NotifierRunnable(final Context context) {
|
|
||||||
mContentResolver = context.getContentResolver();
|
|
||||||
mDB = BrowserDB.from(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Set<String> visitedSet = mVisitedCache.get();
|
|
||||||
if (visitedSet == null) {
|
|
||||||
// The cache was wiped. Repopulate it.
|
|
||||||
Log.w(LOGTAG, "Rebuilding visited link set...");
|
|
||||||
final long start = SystemClock.uptimeMillis();
|
|
||||||
final Cursor c = mDB.getAllVisitedHistory(mContentResolver);
|
|
||||||
if (c == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
visitedSet = new HashSet<String>();
|
|
||||||
if (c.moveToFirst()) {
|
|
||||||
do {
|
|
||||||
visitedSet.add(c.getString(0));
|
|
||||||
} while (c.moveToNext());
|
|
||||||
}
|
|
||||||
mVisitedCache = new SoftReference<Set<String>>(visitedSet);
|
|
||||||
final long end = SystemClock.uptimeMillis();
|
|
||||||
final long took = end - start;
|
|
||||||
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
|
|
||||||
} finally {
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This runs on the same handler thread as the checkUriVisited code,
|
|
||||||
// so no synchronization is needed.
|
|
||||||
while (true) {
|
|
||||||
final String uri = mPendingUris.poll();
|
|
||||||
if (uri == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visitedSet.contains(uri)) {
|
|
||||||
GeckoAppShell.notifyUriVisited(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mProcessing = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private GlobalHistory() {
|
|
||||||
mHandler = ThreadUtils.getBackgroundHandler();
|
|
||||||
mPendingUris = new LinkedList<String>();
|
|
||||||
mVisitedCache = new SoftReference<Set<String>>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addToGeckoOnly(String uri) {
|
|
||||||
Set<String> visitedSet = mVisitedCache.get();
|
|
||||||
if (visitedSet != null) {
|
|
||||||
visitedSet.add(uri);
|
|
||||||
}
|
|
||||||
GeckoAppShell.notifyUriVisited(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(final Context context, final BrowserDB db, String uri) {
|
|
||||||
ThreadUtils.assertOnBackgroundThread();
|
|
||||||
final long start = SystemClock.uptimeMillis();
|
|
||||||
|
|
||||||
// stripAboutReaderUrl only removes about:reader if present, in all other cases the original string is returned
|
|
||||||
final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri);
|
|
||||||
|
|
||||||
db.updateVisitedHistory(context.getContentResolver(), uriToStore);
|
|
||||||
|
|
||||||
final long end = SystemClock.uptimeMillis();
|
|
||||||
final long took = end - start;
|
|
||||||
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
|
|
||||||
addToGeckoOnly(uriToStore);
|
|
||||||
dispatchUriAvailableMessage(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("static-method")
|
|
||||||
public void update(final ContentResolver cr, final BrowserDB db, String uri, String title) {
|
|
||||||
ThreadUtils.assertOnBackgroundThread();
|
|
||||||
final long start = SystemClock.uptimeMillis();
|
|
||||||
|
|
||||||
final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri);
|
|
||||||
|
|
||||||
db.updateHistoryTitle(cr, uriToStore, title);
|
|
||||||
|
|
||||||
final long end = SystemClock.uptimeMillis();
|
|
||||||
final long took = end - start;
|
|
||||||
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkUriVisited(final String uri) {
|
|
||||||
final String storedURI = ReaderModeUtils.stripAboutReaderUrl(uri);
|
|
||||||
|
|
||||||
final NotifierRunnable runnable = new NotifierRunnable(GeckoAppShell.getContext());
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// this runs on the same handler thread as the processing loop,
|
|
||||||
// so no synchronization needed
|
|
||||||
mPendingUris.add(storedURI);
|
|
||||||
if (mProcessing) {
|
|
||||||
// there's already a runnable queued up or working away, so
|
|
||||||
// no need to post another
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mProcessing = true;
|
|
||||||
mHandler.postDelayed(runnable, BATCHING_DELAY_MS);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dispatchUriAvailableMessage(String uri) {
|
|
||||||
final Bundle message = new Bundle();
|
|
||||||
message.putString(EVENT_PARAM_URI, uri);
|
|
||||||
EventDispatcher.getInstance().dispatch(EVENT_URI_AVAILABLE_IN_HISTORY, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.VisibleForTesting;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.db.BrowserContract;
|
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
|
||||||
import org.mozilla.gecko.util.BundleEventListener;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides access to metadata information about websites.
|
|
||||||
*
|
|
||||||
* While storing, in case of timing issues preventing us from looking up History GUID by a given uri,
|
|
||||||
* we queue up metadata and wait for GlobalHistory to let us know history record is now available.
|
|
||||||
*
|
|
||||||
* TODO Bug 1313515: selection of metadata for a given uri/history_GUID
|
|
||||||
*
|
|
||||||
* @author grisha
|
|
||||||
*/
|
|
||||||
/* package-local */ class GlobalPageMetadata implements BundleEventListener {
|
|
||||||
private static final String LOG_TAG = "GeckoGlobalPageMetadata";
|
|
||||||
|
|
||||||
private static final GlobalPageMetadata instance = new GlobalPageMetadata();
|
|
||||||
|
|
||||||
private static final String KEY_HAS_IMAGE = "hasImage";
|
|
||||||
private static final String KEY_METADATA_JSON = "metadataJSON";
|
|
||||||
|
|
||||||
private static final int MAX_METADATA_QUEUE_SIZE = 15;
|
|
||||||
|
|
||||||
private final Map<String, Bundle> queuedMetadata = Collections.synchronizedMap(new LimitedLinkedHashMap<String, Bundle>());
|
|
||||||
|
|
||||||
public static GlobalPageMetadata getInstance() {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class LimitedLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
|
|
||||||
private static final long serialVersionUID = 6359725112736360244L;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean removeEldestEntry(Entry<K, V> eldest) {
|
|
||||||
if (size() > MAX_METADATA_QUEUE_SIZE) {
|
|
||||||
Log.w(LOG_TAG, "Page metadata queue is full. Dropping oldest metadata.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private GlobalPageMetadata() {}
|
|
||||||
|
|
||||||
public void init() {
|
|
||||||
EventDispatcher
|
|
||||||
.getInstance()
|
|
||||||
.registerBackgroundThreadListener(this, GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) {
|
|
||||||
ThreadUtils.assertOnBackgroundThread();
|
|
||||||
|
|
||||||
// NB: Other than checking that JSON is valid and trimming it,
|
|
||||||
// we do not process metadataJSON in any way, trusting our source.
|
|
||||||
doAddOrQueue(db, contentProviderClient, uri, hasImage, metadataJSON);
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
/*package-local */ void doAddOrQueue(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) {
|
|
||||||
final String preparedMetadataJSON;
|
|
||||||
try {
|
|
||||||
preparedMetadataJSON = prepareJSON(metadataJSON);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(LOG_TAG, "Couldn't process metadata JSON", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't bother queuing this if deletions fails to find a corresponding history record.
|
|
||||||
// If we can't delete metadata because it didn't exist yet, that's OK.
|
|
||||||
if (preparedMetadataJSON.equals("{}")) {
|
|
||||||
final int deleted = db.deletePageMetadata(contentProviderClient, uri);
|
|
||||||
// We could delete none if history record for uri isn't present.
|
|
||||||
// We must delete one if history record for uri is present.
|
|
||||||
if (deleted != 0 && deleted != 1) {
|
|
||||||
throw new IllegalStateException("Deleted unexpected number of page metadata records: " + deleted);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we could insert page metadata, we're done.
|
|
||||||
if (db.insertPageMetadata(contentProviderClient, uri, hasImage, preparedMetadataJSON)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, we need to queue it for future insertion when history record is available.
|
|
||||||
Bundle bundledMetadata = new Bundle();
|
|
||||||
bundledMetadata.putBoolean(KEY_HAS_IMAGE, hasImage);
|
|
||||||
bundledMetadata.putString(KEY_METADATA_JSON, preparedMetadataJSON);
|
|
||||||
queuedMetadata.put(uri, bundledMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
/* package-local */ int getMetadataQueueSize() {
|
|
||||||
return queuedMetadata.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(String event, Bundle message, EventCallback callback) {
|
|
||||||
ThreadUtils.assertOnBackgroundThread();
|
|
||||||
|
|
||||||
if (!GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY.equals(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String uri = message.getString(GlobalHistory.EVENT_PARAM_URI);
|
|
||||||
if (TextUtils.isEmpty(uri)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Bundle bundledMetadata;
|
|
||||||
synchronized (queuedMetadata) {
|
|
||||||
if (!queuedMetadata.containsKey(uri)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bundledMetadata = queuedMetadata.get(uri);
|
|
||||||
queuedMetadata.remove(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertMetadataBundleForUri(uri, bundledMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insertMetadataBundleForUri(String uri, Bundle bundledMetadata) {
|
|
||||||
final boolean hasImage = bundledMetadata.getBoolean(KEY_HAS_IMAGE);
|
|
||||||
final String metadataJSON = bundledMetadata.getString(KEY_METADATA_JSON);
|
|
||||||
|
|
||||||
// Acquire CPC, must be released in this function.
|
|
||||||
final ContentProviderClient contentProviderClient = GeckoAppShell.getApplicationContext()
|
|
||||||
.getContentResolver()
|
|
||||||
.acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);
|
|
||||||
|
|
||||||
// Pre-conditions...
|
|
||||||
if (contentProviderClient == null) {
|
|
||||||
Log.e(LOG_TAG, "Couldn't acquire content provider client");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(metadataJSON)) {
|
|
||||||
Log.e(LOG_TAG, "Metadata bundle contained empty metadata json");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert!
|
|
||||||
try {
|
|
||||||
add(
|
|
||||||
BrowserDB.from(GeckoThread.getActiveProfile()),
|
|
||||||
contentProviderClient,
|
|
||||||
uri, hasImage, metadataJSON
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
contentProviderClient.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String prepareJSON(String json) throws JSONException {
|
|
||||||
return (new JSONObject(json)).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.app.KeyguardManager;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.support.v4.app.NotificationCompat;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
// Utility methods for entering/exiting guest mode.
|
|
||||||
public final class GuestSession {
|
|
||||||
private static final String LOGTAG = "GeckoGuestSession";
|
|
||||||
|
|
||||||
public static final String NOTIFICATION_INTENT = "org.mozilla.gecko.GUEST_SESSION_INPROGRESS";
|
|
||||||
|
|
||||||
private static PendingIntent getNotificationIntent(Context context) {
|
|
||||||
Intent intent = new Intent(NOTIFICATION_INTENT);
|
|
||||||
intent.setClassName(context, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
|
||||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void showNotification(Context context) {
|
|
||||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
|
|
||||||
final Resources res = context.getResources();
|
|
||||||
builder.setContentTitle(res.getString(R.string.guest_browsing_notification_title))
|
|
||||||
.setContentText(res.getString(R.string.guest_browsing_notification_text))
|
|
||||||
.setSmallIcon(R.drawable.alert_guest)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setContentIntent(getNotificationIntent(context));
|
|
||||||
|
|
||||||
final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
manager.notify(R.id.guestNotification, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void hideNotification(Context context) {
|
|
||||||
final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
manager.cancel(R.id.guestNotification);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onNotificationIntentReceived(BrowserApp context) {
|
|
||||||
context.showGuestModeDialog(BrowserApp.GuestModeDialog.LEAVING);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,599 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.overlays.ui.ShareDialog;
|
|
||||||
import org.mozilla.gecko.util.ActivityResultHandler;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
import org.mozilla.gecko.util.GeckoEventListener;
|
|
||||||
import org.mozilla.gecko.util.JSONUtils;
|
|
||||||
import org.mozilla.gecko.util.NativeEventListener;
|
|
||||||
import org.mozilla.gecko.util.NativeJSObject;
|
|
||||||
import org.mozilla.gecko.widget.ExternalIntentDuringPrivateBrowsingPromptFragment;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.Browser;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.app.FragmentActivity;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public final class IntentHelper implements GeckoEventListener,
|
|
||||||
NativeEventListener {
|
|
||||||
|
|
||||||
private static final String LOGTAG = "GeckoIntentHelper";
|
|
||||||
private static final String[] EVENTS = {
|
|
||||||
"Intent:GetHandlers",
|
|
||||||
"Intent:Open",
|
|
||||||
"Intent:OpenForResult",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final String[] NATIVE_EVENTS = {
|
|
||||||
"Intent:OpenNoHandler",
|
|
||||||
};
|
|
||||||
|
|
||||||
// via http://developer.android.com/distribute/tools/promote/linking.html
|
|
||||||
private static String MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id=";
|
|
||||||
private static String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url";
|
|
||||||
|
|
||||||
/** A partial URI to an error page - the encoded error URI should be appended before loading. */
|
|
||||||
private static String UNKNOWN_PROTOCOL_URI_PREFIX = "about:neterror?e=unknownProtocolFound&u=";
|
|
||||||
|
|
||||||
private static IntentHelper instance;
|
|
||||||
|
|
||||||
private final FragmentActivity activity;
|
|
||||||
|
|
||||||
private IntentHelper(final FragmentActivity activity) {
|
|
||||||
this.activity = activity;
|
|
||||||
EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this, EVENTS);
|
|
||||||
EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener) this, NATIVE_EVENTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IntentHelper init(final FragmentActivity activity) {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new IntentHelper(activity);
|
|
||||||
} else {
|
|
||||||
Log.w(LOGTAG, "IntentHelper.init() called twice, ignoring.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void destroy() {
|
|
||||||
if (instance != null) {
|
|
||||||
EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) instance, EVENTS);
|
|
||||||
EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) instance, NATIVE_EVENTS);
|
|
||||||
instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given the inputs to <code>getOpenURIIntent</code>, plus an optional
|
|
||||||
* package name and class name, create and fire an intent to open the
|
|
||||||
* provided URI. If a class name is specified but a package name is not,
|
|
||||||
* we will default to using the current fennec package.
|
|
||||||
*
|
|
||||||
* @param targetURI the string spec of the URI to open.
|
|
||||||
* @param mimeType an optional MIME type string.
|
|
||||||
* @param packageName an optional app package name.
|
|
||||||
* @param className an optional intent class name.
|
|
||||||
* @param action an Android action specifier, such as
|
|
||||||
* <code>Intent.ACTION_SEND</code>.
|
|
||||||
* @param title the title to use in <code>ACTION_SEND</code> intents.
|
|
||||||
* @param showPromptInPrivateBrowsing whether or not the user should be prompted when opening
|
|
||||||
* this uri from private browsing. This should be true
|
|
||||||
* when the user doesn't explicitly choose to open an an
|
|
||||||
* external app (e.g. just clicked a link).
|
|
||||||
* @return true if the activity started successfully or the user was prompted to open the
|
|
||||||
* application; false otherwise.
|
|
||||||
*/
|
|
||||||
public static boolean openUriExternal(String targetURI,
|
|
||||||
String mimeType,
|
|
||||||
String packageName,
|
|
||||||
String className,
|
|
||||||
String action,
|
|
||||||
String title,
|
|
||||||
final boolean showPromptInPrivateBrowsing) {
|
|
||||||
final GeckoAppShell.GeckoInterface gi = GeckoAppShell.getGeckoInterface();
|
|
||||||
final Context activityContext = gi != null ? gi.getActivity() : null;
|
|
||||||
final Context context = activityContext != null ? activityContext : GeckoAppShell.getApplicationContext();
|
|
||||||
final Intent intent = getOpenURIIntent(context, targetURI,
|
|
||||||
mimeType, action, title);
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(className)) {
|
|
||||||
if (!TextUtils.isEmpty(packageName)) {
|
|
||||||
intent.setClassName(packageName, className);
|
|
||||||
} else {
|
|
||||||
// Default to using the fennec app context.
|
|
||||||
intent.setClassName(context, className);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!showPromptInPrivateBrowsing || activityContext == null) {
|
|
||||||
if (activityContext == null) {
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
}
|
|
||||||
return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
|
|
||||||
} else {
|
|
||||||
// Ideally we retrieve the Activity from the calling args, rather than
|
|
||||||
// statically, but since this method is called from Gecko and I'm
|
|
||||||
// unfamiliar with that code, this is a simpler solution.
|
|
||||||
final FragmentActivity fragmentActivity = (FragmentActivity) activityContext;
|
|
||||||
return ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
|
|
||||||
context, fragmentActivity.getSupportFragmentManager(), intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasHandlersForIntent(Intent intent) {
|
|
||||||
try {
|
|
||||||
return !GeckoAppShell.queryIntentActivities(intent).isEmpty();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Log.e(LOGTAG, "Exception in hasHandlersForIntent");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String[] getHandlersForIntent(Intent intent) {
|
|
||||||
final PackageManager pm = GeckoAppShell.getApplicationContext().getPackageManager();
|
|
||||||
try {
|
|
||||||
final List<ResolveInfo> list = GeckoAppShell.queryIntentActivities(intent);
|
|
||||||
|
|
||||||
int numAttr = 4;
|
|
||||||
final String[] ret = new String[list.size() * numAttr];
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
ResolveInfo resolveInfo = list.get(i);
|
|
||||||
ret[i * numAttr] = resolveInfo.loadLabel(pm).toString();
|
|
||||||
if (resolveInfo.isDefault)
|
|
||||||
ret[i * numAttr + 1] = "default";
|
|
||||||
else
|
|
||||||
ret[i * numAttr + 1] = "";
|
|
||||||
ret[i * numAttr + 2] = resolveInfo.activityInfo.applicationInfo.packageName;
|
|
||||||
ret[i * numAttr + 3] = resolveInfo.activityInfo.name;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Log.e(LOGTAG, "Exception in getHandlersForIntent");
|
|
||||||
return new String[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Intent getIntentForActionString(String aAction) {
|
|
||||||
// Default to the view action if no other action as been specified.
|
|
||||||
if (TextUtils.isEmpty(aAction)) {
|
|
||||||
return new Intent(Intent.ACTION_VIEW);
|
|
||||||
}
|
|
||||||
return new Intent(aAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a URI, a MIME type, and a title,
|
|
||||||
* produce a share intent which can be used to query all activities
|
|
||||||
* than can open the specified URI.
|
|
||||||
*
|
|
||||||
* @param context a <code>Context</code> instance.
|
|
||||||
* @param targetURI the string spec of the URI to open.
|
|
||||||
* @param mimeType an optional MIME type string.
|
|
||||||
* @param title the title to use in <code>ACTION_SEND</code> intents.
|
|
||||||
* @return an <code>Intent</code>, or <code>null</code> if none could be
|
|
||||||
* produced.
|
|
||||||
*/
|
|
||||||
public static Intent getShareIntent(final Context context,
|
|
||||||
final String targetURI,
|
|
||||||
final String mimeType,
|
|
||||||
final String title) {
|
|
||||||
Intent shareIntent = getIntentForActionString(Intent.ACTION_SEND);
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_TEXT, targetURI);
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);
|
|
||||||
shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true);
|
|
||||||
|
|
||||||
// Note that EXTRA_TITLE is intended to be used for share dialog
|
|
||||||
// titles. Common usage (e.g., Pocket) suggests that it's sometimes
|
|
||||||
// interpreted as an alternate to EXTRA_SUBJECT, so we include it.
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_TITLE, title);
|
|
||||||
|
|
||||||
if (mimeType != null && mimeType.length() > 0) {
|
|
||||||
shareIntent.setType(mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return shareIntent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a URI, a MIME type, an Android intent "action", and a title,
|
|
||||||
* produce an intent which can be used to start an activity to open
|
|
||||||
* the specified URI.
|
|
||||||
*
|
|
||||||
* @param context a <code>Context</code> instance.
|
|
||||||
* @param targetURI the string spec of the URI to open.
|
|
||||||
* @param mimeType an optional MIME type string.
|
|
||||||
* @param action an Android action specifier, such as
|
|
||||||
* <code>Intent.ACTION_SEND</code>.
|
|
||||||
* @param title the title to use in <code>ACTION_SEND</code> intents.
|
|
||||||
* @return an <code>Intent</code>, or <code>null</code> if none could be
|
|
||||||
* produced.
|
|
||||||
*/
|
|
||||||
static Intent getOpenURIIntent(final Context context,
|
|
||||||
final String targetURI,
|
|
||||||
final String mimeType,
|
|
||||||
final String action,
|
|
||||||
final String title) {
|
|
||||||
|
|
||||||
// The resultant chooser can return non-exported activities in 4.1 and earlier.
|
|
||||||
// https://code.google.com/p/android/issues/detail?id=29535
|
|
||||||
final Intent intent = getOpenURIIntentInner(context, targetURI, mimeType, action, title);
|
|
||||||
|
|
||||||
if (intent != null) {
|
|
||||||
// Some applications use this field to return to the same browser after processing the
|
|
||||||
// Intent. While there is some danger (e.g. denial of service), other major browsers already
|
|
||||||
// use it and so it's the norm.
|
|
||||||
intent.putExtra(Browser.EXTRA_APPLICATION_ID, AppConstants.ANDROID_PACKAGE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Intent getOpenURIIntentInner(final Context context, final String targetURI,
|
|
||||||
final String mimeType, final String action, final String title) {
|
|
||||||
|
|
||||||
if (action.equalsIgnoreCase(Intent.ACTION_SEND)) {
|
|
||||||
Intent shareIntent = getShareIntent(context, targetURI, mimeType, title);
|
|
||||||
return Intent.createChooser(shareIntent,
|
|
||||||
context.getResources().getString(R.string.share_title));
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
|
|
||||||
if (!TextUtils.isEmpty(mimeType)) {
|
|
||||||
Intent intent = getIntentForActionString(action);
|
|
||||||
intent.setDataAndType(uri, mimeType);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GeckoAppShell.isUriSafeForScheme(uri)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String scheme = uri.getScheme();
|
|
||||||
if ("intent".equals(scheme) || "android-app".equals(scheme)) {
|
|
||||||
final Intent intent;
|
|
||||||
try {
|
|
||||||
intent = Intent.parseUri(targetURI, 0);
|
|
||||||
} catch (final URISyntaxException e) {
|
|
||||||
Log.e(LOGTAG, "Unable to parse URI - " + e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Uri data = intent.getData();
|
|
||||||
if (data != null && "file".equals(data.normalizeScheme().getScheme())) {
|
|
||||||
Log.w(LOGTAG, "Blocked intent with \"file://\" data scheme.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only open applications which can accept arbitrary data from a browser.
|
|
||||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
|
||||||
|
|
||||||
// Prevent site from explicitly opening our internal activities, which can leak data.
|
|
||||||
intent.setComponent(null);
|
|
||||||
nullIntentSelector(intent);
|
|
||||||
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute our most likely intent, then check to see if there are any
|
|
||||||
// custom handlers that would apply.
|
|
||||||
// Start with the original URI. If we end up modifying it, we'll
|
|
||||||
// overwrite it.
|
|
||||||
final String extension = MimeTypeMap.getFileExtensionFromUrl(targetURI);
|
|
||||||
final Intent intent = getIntentForActionString(action);
|
|
||||||
intent.setData(uri);
|
|
||||||
|
|
||||||
if ("file".equals(scheme)) {
|
|
||||||
// Only set explicit mimeTypes on file://.
|
|
||||||
final String mimeType2 = GeckoAppShell.getMimeTypeFromExtension(extension);
|
|
||||||
intent.setType(mimeType2);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Have a special handling for SMS based schemes, as the query parameters
|
|
||||||
// are not extracted from the URI automatically.
|
|
||||||
if (!"sms".equals(scheme) && !"smsto".equals(scheme) && !"mms".equals(scheme) && !"mmsto".equals(scheme)) {
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String query = uri.getEncodedQuery();
|
|
||||||
if (TextUtils.isEmpty(query)) {
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is common to see sms*/mms* uris on the web without '//', it is W3C standard not to have the slashes,
|
|
||||||
// but android's Uri builder & Uri require the slashes and will interpret those without as malformed.
|
|
||||||
String currentUri = uri.toString();
|
|
||||||
String correctlyFormattedDataURIScheme = scheme + "://";
|
|
||||||
if (!currentUri.contains(correctlyFormattedDataURIScheme)) {
|
|
||||||
uri = Uri.parse(currentUri.replaceFirst(scheme + ":", correctlyFormattedDataURIScheme));
|
|
||||||
}
|
|
||||||
|
|
||||||
final String[] fields = query.split("&");
|
|
||||||
boolean shouldUpdateIntent = false;
|
|
||||||
String resultQuery = "";
|
|
||||||
for (String field : fields) {
|
|
||||||
if (field.startsWith("body=")) {
|
|
||||||
final String body = Uri.decode(field.substring(5));
|
|
||||||
intent.putExtra("sms_body", body);
|
|
||||||
shouldUpdateIntent = true;
|
|
||||||
} else if (field.startsWith("subject=")) {
|
|
||||||
final String subject = Uri.decode(field.substring(8));
|
|
||||||
intent.putExtra("subject", subject);
|
|
||||||
shouldUpdateIntent = true;
|
|
||||||
} else if (field.startsWith("cc=")) {
|
|
||||||
final String ccNumber = Uri.decode(field.substring(3));
|
|
||||||
String phoneNumber = uri.getAuthority();
|
|
||||||
if (phoneNumber != null) {
|
|
||||||
uri = uri.buildUpon().encodedAuthority(phoneNumber + ";" + ccNumber).build();
|
|
||||||
}
|
|
||||||
shouldUpdateIntent = true;
|
|
||||||
} else {
|
|
||||||
resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldUpdateIntent) {
|
|
||||||
// No need to rewrite the URI, then.
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form a new URI without the extracted fields in the query part, and
|
|
||||||
// push that into the new Intent.
|
|
||||||
final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : "";
|
|
||||||
final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build();
|
|
||||||
intent.setData(pruned);
|
|
||||||
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We create a separate method to better encapsulate the @TargetApi use.
|
|
||||||
@TargetApi(15)
|
|
||||||
private static void nullIntentSelector(final Intent intent) {
|
|
||||||
intent.setSelector(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a <code>Uri</code> instance which is equivalent to <code>u</code>,
|
|
||||||
* but with a guaranteed-lowercase scheme as if the API level 16 method
|
|
||||||
* <code>u.normalizeScheme</code> had been called.
|
|
||||||
*
|
|
||||||
* @param u the <code>Uri</code> to normalize.
|
|
||||||
* @return a <code>Uri</code>, which might be <code>u</code>.
|
|
||||||
*/
|
|
||||||
private static Uri normalizeUriScheme(final Uri u) {
|
|
||||||
final String scheme = u.getScheme();
|
|
||||||
final String lower = scheme.toLowerCase(Locale.US);
|
|
||||||
if (lower.equals(scheme)) {
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, return a new URI with a normalized scheme.
|
|
||||||
return u.buildUpon().scheme(lower).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
|
|
||||||
if (event.equals("Intent:OpenNoHandler")) {
|
|
||||||
openNoHandler(message, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(String event, JSONObject message) {
|
|
||||||
try {
|
|
||||||
if (event.equals("Intent:GetHandlers")) {
|
|
||||||
getHandlers(message);
|
|
||||||
} else if (event.equals("Intent:Open")) {
|
|
||||||
open(message);
|
|
||||||
} else if (event.equals("Intent:OpenForResult")) {
|
|
||||||
openForResult(message);
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getHandlers(JSONObject message) throws JSONException {
|
|
||||||
final Intent intent = getOpenURIIntent(activity,
|
|
||||||
message.optString("url"),
|
|
||||||
message.optString("mime"),
|
|
||||||
message.optString("action"),
|
|
||||||
message.optString("title"));
|
|
||||||
final List<String> appList = Arrays.asList(getHandlersForIntent(intent));
|
|
||||||
|
|
||||||
final JSONObject response = new JSONObject();
|
|
||||||
response.put("apps", new JSONArray(appList));
|
|
||||||
EventDispatcher.sendResponse(message, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void open(JSONObject message) throws JSONException {
|
|
||||||
openUriExternal(message.optString("url"),
|
|
||||||
message.optString("mime"),
|
|
||||||
message.optString("packageName"),
|
|
||||||
message.optString("className"),
|
|
||||||
message.optString("action"),
|
|
||||||
message.optString("title"), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openForResult(final JSONObject message) throws JSONException {
|
|
||||||
Intent intent = getOpenURIIntent(activity,
|
|
||||||
message.optString("url"),
|
|
||||||
message.optString("mime"),
|
|
||||||
message.optString("action"),
|
|
||||||
message.optString("title"));
|
|
||||||
intent.setClassName(message.optString("packageName"), message.optString("className"));
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
||||||
|
|
||||||
final ResultHandler handler = new ResultHandler(message);
|
|
||||||
try {
|
|
||||||
ActivityHandlerHelper.startIntentForActivity(activity, intent, handler);
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
Log.w(LOGTAG, "Forbidden to launch activity.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a URI without any valid handlers on device. In the best case, a package is specified
|
|
||||||
* and we can bring the user directly to the application page in an app market. If a package is
|
|
||||||
* not specified and there is a fallback url in the intent extras, we open that url. If neither
|
|
||||||
* is present, we alert the user that we were unable to open the link.
|
|
||||||
*
|
|
||||||
* @param msg A message with the uri with no handlers as the value for the "uri" key
|
|
||||||
* @param callback A callback that will be called with success & no params if Java loads a page, or with error and
|
|
||||||
* the uri to load if Java does not load a page
|
|
||||||
*/
|
|
||||||
private void openNoHandler(final NativeJSObject msg, final EventCallback callback) {
|
|
||||||
final String uri = msg.getString("uri");
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(uri)) {
|
|
||||||
Log.w(LOGTAG, "Received empty URL - loading about:neterror");
|
|
||||||
callback.sendError(getUnknownProtocolErrorPageUri(""));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Intent intent;
|
|
||||||
try {
|
|
||||||
// TODO (bug 1173626): This will not handle android-app uris on non 5.1 devices.
|
|
||||||
intent = Intent.parseUri(uri, 0);
|
|
||||||
} catch (final URISyntaxException e) {
|
|
||||||
String errorUri;
|
|
||||||
try {
|
|
||||||
errorUri = getUnknownProtocolErrorPageUri(URLEncoder.encode(uri, "UTF-8"));
|
|
||||||
} catch (final UnsupportedEncodingException encodingE) {
|
|
||||||
errorUri = getUnknownProtocolErrorPageUri("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't log the exception to prevent leaking URIs.
|
|
||||||
Log.w(LOGTAG, "Unable to parse Intent URI - loading about:neterror");
|
|
||||||
callback.sendError(errorUri);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For this flow, we follow Chrome's lead:
|
|
||||||
// https://developer.chrome.com/multidevice/android/intents
|
|
||||||
final String fallbackUrl = intent.getStringExtra(EXTRA_BROWSER_FALLBACK_URL);
|
|
||||||
if (isFallbackUrlValid(fallbackUrl)) {
|
|
||||||
// Opens the page in JS.
|
|
||||||
callback.sendError(fallbackUrl);
|
|
||||||
|
|
||||||
} else if (intent.getPackage() != null) {
|
|
||||||
// Note on alternative flows: we could get the intent package from a component, however, for
|
|
||||||
// security reasons, components are ignored when opening URIs (bug 1168998) so we should
|
|
||||||
// ignore it here too.
|
|
||||||
//
|
|
||||||
// Our old flow used to prompt the user to search for their app in the market by scheme and
|
|
||||||
// while this could help the user find a new app, there is not always a correlation in
|
|
||||||
// scheme to application name and we could end up steering the user wrong (potentially to
|
|
||||||
// malicious software). Better to leave that one alone.
|
|
||||||
final String marketUri = MARKET_INTENT_URI_PACKAGE_PREFIX + intent.getPackage();
|
|
||||||
final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(marketUri));
|
|
||||||
marketIntent.addCategory(Intent.CATEGORY_BROWSABLE);
|
|
||||||
marketIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
|
|
||||||
// (Bug 1192436) We don't know if marketIntent matches any Activities (e.g. non-Play
|
|
||||||
// Store devices). If it doesn't, clicking the link will cause no action to occur.
|
|
||||||
ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
|
|
||||||
activity, activity.getSupportFragmentManager(), marketIntent);
|
|
||||||
callback.sendSuccess(null);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// We return the error page here, but it will only be shown if we think the load did
|
|
||||||
// not come from clicking a link. Chrome does not show error pages in that case, and
|
|
||||||
// many websites have catered to this behavior. For example, the site might set a timeout and load a play
|
|
||||||
// store url for their app if the intent link fails to load, i.e. the app is not installed.
|
|
||||||
// These work-arounds would often end with our users seeing about:neterror instead of the intended experience.
|
|
||||||
// While I feel showing about:neterror is a better solution for users (when not hacked around),
|
|
||||||
// we should match the status quo for the good of our users.
|
|
||||||
//
|
|
||||||
// Don't log the URI to prevent leaking it.
|
|
||||||
Log.w(LOGTAG, "Unable to open URI, maybe showing neterror");
|
|
||||||
callback.sendError(getUnknownProtocolErrorPageUri(intent.getData().toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isFallbackUrlValid(@Nullable final String fallbackUrl) {
|
|
||||||
if (fallbackUrl == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final String anyCaseScheme = new URI(fallbackUrl).getScheme();
|
|
||||||
final String scheme = (anyCaseScheme == null) ? null : anyCaseScheme.toLowerCase(Locale.US);
|
|
||||||
if ("http".equals(scheme) || "https".equals(scheme)) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
Log.w(LOGTAG, "Fallback URI uses unsupported scheme: " + scheme + ". Try http or https.");
|
|
||||||
}
|
|
||||||
} catch (final URISyntaxException e) {
|
|
||||||
// Do not include Exception to avoid leaking uris.
|
|
||||||
Log.w(LOGTAG, "URISyntaxException parsing fallback URI");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an about:neterror uri with the unknownProtocolFound text as a parameter.
|
|
||||||
* @param encodedUri The encoded uri. While the page does not open correctly without specifying
|
|
||||||
* a uri parameter, it happily accepts the empty String so this argument may
|
|
||||||
* be the empty String.
|
|
||||||
*/
|
|
||||||
private String getUnknownProtocolErrorPageUri(final String encodedUri) {
|
|
||||||
return UNKNOWN_PROTOCOL_URI_PREFIX + encodedUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ResultHandler implements ActivityResultHandler {
|
|
||||||
private final JSONObject message;
|
|
||||||
|
|
||||||
public ResultHandler(JSONObject message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int resultCode, Intent data) {
|
|
||||||
JSONObject response = new JSONObject();
|
|
||||||
try {
|
|
||||||
if (data != null) {
|
|
||||||
if (data.getExtras() != null) {
|
|
||||||
response.put("extras", JSONUtils.bundleToJSON(data.getExtras()));
|
|
||||||
}
|
|
||||||
if (data.getData() != null) {
|
|
||||||
response.put("uri", data.getData().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.put("resultCode", resultCode);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.w(LOGTAG, "Error building JSON response.", e);
|
|
||||||
}
|
|
||||||
EventDispatcher.sendResponse(message, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.customtabs.CustomTabsIntent;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.customtabs.CustomTabsActivity;
|
|
||||||
import org.mozilla.gecko.db.BrowserContract;
|
|
||||||
import org.mozilla.gecko.mozglue.SafeIntent;
|
|
||||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
|
||||||
import org.mozilla.gecko.tabqueue.TabQueueHelper;
|
|
||||||
import org.mozilla.gecko.tabqueue.TabQueueService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity that receives incoming Intents and dispatches them to the appropriate activities (e.g. browser, custom tabs, web app).
|
|
||||||
*/
|
|
||||||
public class LauncherActivity extends Activity {
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
GeckoAppShell.ensureCrashHandling();
|
|
||||||
|
|
||||||
final SafeIntent safeIntent = new SafeIntent(getIntent());
|
|
||||||
|
|
||||||
// If it's not a view intent, it won't be a custom tabs intent either. Just launch!
|
|
||||||
if (!isViewIntentWithURL(safeIntent)) {
|
|
||||||
dispatchNormalIntent();
|
|
||||||
|
|
||||||
// Is this a custom tabs intent, and are custom tabs enabled?
|
|
||||||
} else if (AppConstants.MOZ_ANDROID_CUSTOM_TABS && isCustomTabsIntent(safeIntent)
|
|
||||||
&& isCustomTabsEnabled()) {
|
|
||||||
dispatchCustomTabsIntent();
|
|
||||||
|
|
||||||
// Can we dispatch this VIEW action intent to the tab queue service?
|
|
||||||
} else if (!safeIntent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false)
|
|
||||||
&& TabQueueHelper.TAB_QUEUE_ENABLED
|
|
||||||
&& TabQueueHelper.isTabQueueEnabled(this)) {
|
|
||||||
dispatchTabQueueIntent();
|
|
||||||
|
|
||||||
// Dispatch this VIEW action intent to the browser.
|
|
||||||
} else {
|
|
||||||
dispatchNormalIntent();
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch tab queue service to display overlay.
|
|
||||||
*/
|
|
||||||
private void dispatchTabQueueIntent() {
|
|
||||||
Intent intent = new Intent(getIntent());
|
|
||||||
intent.setClass(getApplicationContext(), TabQueueService.class);
|
|
||||||
startService(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch the browser activity.
|
|
||||||
*/
|
|
||||||
private void dispatchNormalIntent() {
|
|
||||||
Intent intent = new Intent(getIntent());
|
|
||||||
intent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
|
||||||
|
|
||||||
filterFlags(intent);
|
|
||||||
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dispatchCustomTabsIntent() {
|
|
||||||
Intent intent = new Intent(getIntent());
|
|
||||||
intent.setClassName(getApplicationContext(), CustomTabsActivity.class.getName());
|
|
||||||
|
|
||||||
filterFlags(intent);
|
|
||||||
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void filterFlags(Intent intent) {
|
|
||||||
// Explicitly remove the new task and clear task flags (Our browser activity is a single
|
|
||||||
// task activity and we never want to start a second task here). See bug 1280112.
|
|
||||||
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
|
|
||||||
// LauncherActivity is started with the "exclude from recents" flag (set in manifest). We do
|
|
||||||
// not want to propagate this flag from the launcher activity to the browser.
|
|
||||||
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isViewIntentWithURL(@NonNull final SafeIntent safeIntent) {
|
|
||||||
return Intent.ACTION_VIEW.equals(safeIntent.getAction())
|
|
||||||
&& safeIntent.getDataString() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isCustomTabsIntent(@NonNull final SafeIntent safeIntent) {
|
|
||||||
return isViewIntentWithURL(safeIntent)
|
|
||||||
&& safeIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCustomTabsEnabled() {
|
|
||||||
return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_CUSTOM_TABS, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implement this interface to provide Fennec's locale switching functionality.
|
|
||||||
*
|
|
||||||
* The LocaleManager is responsible for persisting and applying selected locales,
|
|
||||||
* and correcting configurations after Android has changed them.
|
|
||||||
*/
|
|
||||||
public interface LocaleManager {
|
|
||||||
void initialize(Context context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if locale switching is enabled.
|
|
||||||
*/
|
|
||||||
boolean isEnabled();
|
|
||||||
Locale getCurrentLocale(Context context);
|
|
||||||
String getAndApplyPersistedLocale(Context context);
|
|
||||||
void correctLocale(Context context, Resources resources, Configuration newConfig);
|
|
||||||
void updateConfiguration(Context context, Locale locale);
|
|
||||||
String setSelectedLocale(Context context, String localeCode);
|
|
||||||
boolean systemLocaleDidChange();
|
|
||||||
void resetToSystemLocale(Context context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this in your onConfigurationChanged handler. This method is expected
|
|
||||||
* to do the appropriate thing: if the user has selected a locale, it
|
|
||||||
* corrects the incoming configuration; if not, it signals the new locale to
|
|
||||||
* use.
|
|
||||||
*/
|
|
||||||
Locale onSystemConfigurationChanged(Context context, Resources resources, Configuration configuration, Locale currentActivityLocale);
|
|
||||||
String getFallbackLocaleTag();
|
|
||||||
}
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.LocaleManager;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.StrictMode;
|
|
||||||
import android.support.v4.app.FragmentActivity;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a helper class to do typical locale switching operations without
|
|
||||||
* hitting StrictMode errors or adding boilerplate to common activity
|
|
||||||
* subclasses.
|
|
||||||
*
|
|
||||||
* Either call {@link Locales#initializeLocale(Context)} in your
|
|
||||||
* <code>onCreate</code> method, or inherit from
|
|
||||||
* <code>LocaleAwareFragmentActivity</code> or <code>LocaleAwareActivity</code>.
|
|
||||||
*/
|
|
||||||
public class Locales {
|
|
||||||
public static LocaleManager getLocaleManager() {
|
|
||||||
try {
|
|
||||||
final Class<?> clazz = Class.forName("org.mozilla.gecko.BrowserLocaleManager");
|
|
||||||
final Method getInstance = clazz.getMethod("getInstance");
|
|
||||||
final LocaleManager localeManager = (LocaleManager) getInstance.invoke(null);
|
|
||||||
return localeManager;
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initializeLocale(Context context) {
|
|
||||||
final LocaleManager localeManager = getLocaleManager();
|
|
||||||
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
|
|
||||||
StrictMode.allowThreadDiskWrites();
|
|
||||||
try {
|
|
||||||
localeManager.getAndApplyPersistedLocale(context);
|
|
||||||
} finally {
|
|
||||||
StrictMode.setThreadPolicy(savedPolicy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static abstract class LocaleAwareAppCompatActivity extends AppCompatActivity {
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
Locales.initializeLocale(getApplicationContext());
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
public static abstract class LocaleAwareFragmentActivity extends FragmentActivity {
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
Locales.initializeLocale(getApplicationContext());
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static abstract class LocaleAwareActivity extends Activity {
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
Locales.initializeLocale(getApplicationContext());
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sometimes we want just the language for a locale, not the entire language
|
|
||||||
* tag. But Java's .getLanguage method is wrong.
|
|
||||||
*
|
|
||||||
* This method is equivalent to the first part of
|
|
||||||
* {@link Locales#getLanguageTag(Locale)}.
|
|
||||||
*
|
|
||||||
* @return a language string, such as "he" for the Hebrew locales.
|
|
||||||
*/
|
|
||||||
public static String getLanguage(final Locale locale) {
|
|
||||||
// Can, but should never be, an empty string.
|
|
||||||
final String language = locale.getLanguage();
|
|
||||||
|
|
||||||
// Modernize certain language codes.
|
|
||||||
if (language.equals("iw")) {
|
|
||||||
return "he";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (language.equals("in")) {
|
|
||||||
return "id";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (language.equals("ji")) {
|
|
||||||
return "yi";
|
|
||||||
}
|
|
||||||
|
|
||||||
return language;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gecko uses locale codes like "es-ES", whereas a Java {@link Locale}
|
|
||||||
* stringifies as "es_ES".
|
|
||||||
*
|
|
||||||
* This method approximates the Java 7 method
|
|
||||||
* <code>Locale#toLanguageTag()</code>.
|
|
||||||
*
|
|
||||||
* @return a locale string suitable for passing to Gecko.
|
|
||||||
*/
|
|
||||||
public static String getLanguageTag(final Locale locale) {
|
|
||||||
// If this were Java 7:
|
|
||||||
// return locale.toLanguageTag();
|
|
||||||
|
|
||||||
final String language = getLanguage(locale);
|
|
||||||
final String country = locale.getCountry(); // Can be an empty string.
|
|
||||||
if (country.equals("")) {
|
|
||||||
return language;
|
|
||||||
}
|
|
||||||
return language + "-" + country;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Locale parseLocaleCode(final String localeCode) {
|
|
||||||
int index;
|
|
||||||
if ((index = localeCode.indexOf('-')) != -1 ||
|
|
||||||
(index = localeCode.indexOf('_')) != -1) {
|
|
||||||
final String langCode = localeCode.substring(0, index);
|
|
||||||
final String countryCode = localeCode.substring(index + 1);
|
|
||||||
return new Locale(langCode, countryCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Locale(localeCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
/* 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.GeckoApp;
|
|
||||||
import org.mozilla.gecko.util.GeckoEventListener;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
public class MediaCastingBar extends RelativeLayout implements View.OnClickListener, GeckoEventListener {
|
|
||||||
private static final String LOGTAG = "GeckoMediaCastingBar";
|
|
||||||
|
|
||||||
private TextView mCastingTo;
|
|
||||||
private ImageButton mMediaPlay;
|
|
||||||
private ImageButton mMediaPause;
|
|
||||||
private ImageButton mMediaStop;
|
|
||||||
|
|
||||||
private boolean mInflated;
|
|
||||||
|
|
||||||
public MediaCastingBar(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
|
|
||||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
|
|
||||||
"Casting:Started",
|
|
||||||
"Casting:Paused",
|
|
||||||
"Casting:Playing",
|
|
||||||
"Casting:Stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void inflateContent() {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
|
||||||
View content = inflater.inflate(R.layout.media_casting, this);
|
|
||||||
|
|
||||||
mMediaPlay = (ImageButton) content.findViewById(R.id.media_play);
|
|
||||||
mMediaPlay.setOnClickListener(this);
|
|
||||||
mMediaPause = (ImageButton) content.findViewById(R.id.media_pause);
|
|
||||||
mMediaPause.setOnClickListener(this);
|
|
||||||
mMediaStop = (ImageButton) content.findViewById(R.id.media_stop);
|
|
||||||
mMediaStop.setOnClickListener(this);
|
|
||||||
|
|
||||||
mCastingTo = (TextView) content.findViewById(R.id.media_sending_to);
|
|
||||||
|
|
||||||
// Capture clicks on the rest of the view to prevent them from
|
|
||||||
// leaking into other views positioned below.
|
|
||||||
content.setOnClickListener(this);
|
|
||||||
|
|
||||||
mInflated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show() {
|
|
||||||
if (!mInflated)
|
|
||||||
inflateContent();
|
|
||||||
|
|
||||||
setVisibility(VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void hide() {
|
|
||||||
setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onDestroy() {
|
|
||||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
|
|
||||||
"Casting:Started",
|
|
||||||
"Casting:Paused",
|
|
||||||
"Casting:Playing",
|
|
||||||
"Casting:Stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
// View.OnClickListener implementation
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
final int viewId = v.getId();
|
|
||||||
|
|
||||||
if (viewId == R.id.media_play) {
|
|
||||||
GeckoAppShell.notifyObservers("Casting:Play", "");
|
|
||||||
mMediaPlay.setVisibility(GONE);
|
|
||||||
mMediaPause.setVisibility(VISIBLE);
|
|
||||||
} else if (viewId == R.id.media_pause) {
|
|
||||||
GeckoAppShell.notifyObservers("Casting:Pause", "");
|
|
||||||
mMediaPause.setVisibility(GONE);
|
|
||||||
mMediaPlay.setVisibility(VISIBLE);
|
|
||||||
} else if (viewId == R.id.media_stop) {
|
|
||||||
GeckoAppShell.notifyObservers("Casting:Stop", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GeckoEventListener implementation
|
|
||||||
@Override
|
|
||||||
public void handleMessage(final String event, final JSONObject message) {
|
|
||||||
final String device = message.optString("device");
|
|
||||||
|
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (event.equals("Casting:Started")) {
|
|
||||||
show();
|
|
||||||
if (!TextUtils.isEmpty(device)) {
|
|
||||||
mCastingTo.setText(device);
|
|
||||||
} else {
|
|
||||||
// Should not happen
|
|
||||||
mCastingTo.setText("");
|
|
||||||
Log.d(LOGTAG, "Device name is empty.");
|
|
||||||
}
|
|
||||||
mMediaPlay.setVisibility(GONE);
|
|
||||||
mMediaPause.setVisibility(VISIBLE);
|
|
||||||
} else if (event.equals("Casting:Paused")) {
|
|
||||||
mMediaPause.setVisibility(GONE);
|
|
||||||
mMediaPlay.setVisibility(VISIBLE);
|
|
||||||
} else if (event.equals("Casting:Playing")) {
|
|
||||||
mMediaPlay.setVisibility(GONE);
|
|
||||||
mMediaPause.setVisibility(VISIBLE);
|
|
||||||
} else if (event.equals("Casting:Stopped")) {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,323 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.support.v7.media.MediaControlIntent;
|
|
||||||
import android.support.v7.media.MediaRouteSelector;
|
|
||||||
import android.support.v7.media.MediaRouter;
|
|
||||||
import android.support.v7.media.MediaRouter.RouteInfo;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.annotation.JNITarget;
|
|
||||||
import org.mozilla.gecko.annotation.ReflectionTarget;
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
import org.mozilla.gecko.util.NativeEventListener;
|
|
||||||
import org.mozilla.gecko.util.NativeJSObject;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages a list of GeckoMediaPlayers methods (i.e. Chromecast/Miracast). Routes messages
|
|
||||||
* from Gecko to the correct caster based on the id of the display
|
|
||||||
*/
|
|
||||||
public class MediaPlayerManager extends Fragment implements NativeEventListener {
|
|
||||||
/**
|
|
||||||
* Create a new instance of DetailsFragment, initialized to
|
|
||||||
* show the text at 'index'.
|
|
||||||
*/
|
|
||||||
|
|
||||||
private static MediaPlayerManager instance = null;
|
|
||||||
|
|
||||||
@ReflectionTarget
|
|
||||||
public static MediaPlayerManager getInstance() {
|
|
||||||
if (instance != null) {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
if (Versions.feature17Plus) {
|
|
||||||
instance = (MediaPlayerManager) new PresentationMediaPlayerManager();
|
|
||||||
} else {
|
|
||||||
instance = new MediaPlayerManager();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String LOGTAG = "GeckoMediaPlayerManager";
|
|
||||||
protected boolean isPresentationMode = false; // Used to prevent mirroring when Presentation API is used.
|
|
||||||
|
|
||||||
@ReflectionTarget
|
|
||||||
public static final String MEDIA_PLAYER_TAG = "MPManagerFragment";
|
|
||||||
|
|
||||||
private static final boolean SHOW_DEBUG = false;
|
|
||||||
// Simplified debugging interfaces
|
|
||||||
private static void debug(String msg, Exception e) {
|
|
||||||
if (SHOW_DEBUG) {
|
|
||||||
Log.e(LOGTAG, msg, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void debug(String msg) {
|
|
||||||
if (SHOW_DEBUG) {
|
|
||||||
Log.d(LOGTAG, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MediaRouter mediaRouter = null;
|
|
||||||
protected final Map<String, GeckoMediaPlayer> players = new HashMap<String, GeckoMediaPlayer>();
|
|
||||||
protected final Map<String, GeckoPresentationDisplay> displays = new HashMap<String, GeckoPresentationDisplay>(); // used for Presentation API
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
|
|
||||||
"MediaPlayer:Load",
|
|
||||||
"MediaPlayer:Start",
|
|
||||||
"MediaPlayer:Stop",
|
|
||||||
"MediaPlayer:Play",
|
|
||||||
"MediaPlayer:Pause",
|
|
||||||
"MediaPlayer:End",
|
|
||||||
"MediaPlayer:Mirror",
|
|
||||||
"MediaPlayer:Message",
|
|
||||||
"AndroidCastDevice:Start",
|
|
||||||
"AndroidCastDevice:Stop",
|
|
||||||
"AndroidCastDevice:SyncDevice");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@JNITarget
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
|
|
||||||
"MediaPlayer:Load",
|
|
||||||
"MediaPlayer:Start",
|
|
||||||
"MediaPlayer:Stop",
|
|
||||||
"MediaPlayer:Play",
|
|
||||||
"MediaPlayer:Pause",
|
|
||||||
"MediaPlayer:End",
|
|
||||||
"MediaPlayer:Mirror",
|
|
||||||
"MediaPlayer:Message",
|
|
||||||
"AndroidCastDevice:Start",
|
|
||||||
"AndroidCastDevice:Stop",
|
|
||||||
"AndroidCastDevice:SyncDevice");
|
|
||||||
}
|
|
||||||
|
|
||||||
// GeckoEventListener implementation
|
|
||||||
@Override
|
|
||||||
public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) {
|
|
||||||
debug(event);
|
|
||||||
if (event.startsWith("MediaPlayer:")) {
|
|
||||||
final GeckoMediaPlayer player = players.get(message.getString("id"));
|
|
||||||
if (player == null) {
|
|
||||||
Log.e(LOGTAG, "Couldn't find a player for this id: " + message.getString("id") + " for message: " + event);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.sendError(null);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("MediaPlayer:Play".equals(event)) {
|
|
||||||
player.play(callback);
|
|
||||||
} else if ("MediaPlayer:Start".equals(event)) {
|
|
||||||
player.start(callback);
|
|
||||||
} else if ("MediaPlayer:Stop".equals(event)) {
|
|
||||||
player.stop(callback);
|
|
||||||
} else if ("MediaPlayer:Pause".equals(event)) {
|
|
||||||
player.pause(callback);
|
|
||||||
} else if ("MediaPlayer:End".equals(event)) {
|
|
||||||
player.end(callback);
|
|
||||||
} else if ("MediaPlayer:Mirror".equals(event)) {
|
|
||||||
player.mirror(callback);
|
|
||||||
} else if ("MediaPlayer:Message".equals(event) && message.has("data")) {
|
|
||||||
player.message(message.getString("data"), callback);
|
|
||||||
} else if ("MediaPlayer:Load".equals(event)) {
|
|
||||||
final String url = message.optString("source", "");
|
|
||||||
final String type = message.optString("type", "video/mp4");
|
|
||||||
final String title = message.optString("title", "");
|
|
||||||
player.load(title, url, type, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.startsWith("AndroidCastDevice:")) {
|
|
||||||
if ("AndroidCastDevice:Start".equals(event)) {
|
|
||||||
final GeckoPresentationDisplay display = displays.get(message.getString("id"));
|
|
||||||
if (display == null) {
|
|
||||||
Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
display.start(callback);
|
|
||||||
} else if ("AndroidCastDevice:Stop".equals(event)) {
|
|
||||||
final GeckoPresentationDisplay display = displays.get(message.getString("id"));
|
|
||||||
if (display == null) {
|
|
||||||
Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
display.stop(callback);
|
|
||||||
} else if ("AndroidCastDevice:SyncDevice".equals(event)) {
|
|
||||||
for (Map.Entry<String, GeckoPresentationDisplay> entry : displays.entrySet()) {
|
|
||||||
GeckoPresentationDisplay display = entry.getValue();
|
|
||||||
JSONObject json = display.toJSON();
|
|
||||||
if (json == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
GeckoAppShell.notifyObservers("AndroidCastDevice:Added", json.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final MediaRouter.Callback callback =
|
|
||||||
new MediaRouter.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onRouteRemoved(MediaRouter router, RouteInfo route) {
|
|
||||||
debug("onRouteRemoved: route=" + route);
|
|
||||||
|
|
||||||
// Remove from media player list.
|
|
||||||
players.remove(route.getId());
|
|
||||||
GeckoAppShell.notifyObservers("MediaPlayer:Removed", route.getId());
|
|
||||||
updatePresentation();
|
|
||||||
|
|
||||||
// Remove from presentation display list.
|
|
||||||
displays.remove(route.getId());
|
|
||||||
GeckoAppShell.notifyObservers("AndroidCastDevice:Removed", route.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) {
|
|
||||||
updatePresentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// These methods aren't used by the support version Media Router
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
|
|
||||||
updatePresentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
|
|
||||||
updatePresentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
|
|
||||||
debug("onRouteAdded: route=" + route);
|
|
||||||
final GeckoMediaPlayer player = getMediaPlayerForRoute(route);
|
|
||||||
saveAndNotifyOfPlayer("MediaPlayer:Added", route, player);
|
|
||||||
updatePresentation();
|
|
||||||
|
|
||||||
final GeckoPresentationDisplay display = getPresentationDisplayForRoute(route);
|
|
||||||
saveAndNotifyOfDisplay("AndroidCastDevice:Added", route, display);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
|
|
||||||
debug("onRouteChanged: route=" + route);
|
|
||||||
final GeckoMediaPlayer player = players.get(route.getId());
|
|
||||||
saveAndNotifyOfPlayer("MediaPlayer:Changed", route, player);
|
|
||||||
updatePresentation();
|
|
||||||
|
|
||||||
final GeckoPresentationDisplay display = displays.get(route.getId());
|
|
||||||
saveAndNotifyOfDisplay("AndroidCastDevice:Changed", route, display);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveAndNotifyOfPlayer(final String eventName,
|
|
||||||
MediaRouter.RouteInfo route,
|
|
||||||
final GeckoMediaPlayer player) {
|
|
||||||
if (player == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final JSONObject json = player.toJSON();
|
|
||||||
if (json == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
players.put(route.getId(), player);
|
|
||||||
GeckoAppShell.notifyObservers(eventName, json.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveAndNotifyOfDisplay(final String eventName,
|
|
||||||
MediaRouter.RouteInfo route,
|
|
||||||
final GeckoPresentationDisplay display) {
|
|
||||||
if (display == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final JSONObject json = display.toJSON();
|
|
||||||
if (json == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
displays.put(route.getId(), display);
|
|
||||||
GeckoAppShell.notifyObservers(eventName, json.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) {
|
|
||||||
try {
|
|
||||||
if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
|
|
||||||
return new ChromeCastPlayer(getActivity(), route);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
debug("Error handling presentation", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GeckoPresentationDisplay getPresentationDisplayForRoute(MediaRouter.RouteInfo route) {
|
|
||||||
try {
|
|
||||||
if (route.supportsControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))) {
|
|
||||||
return new ChromeCastDisplay(getActivity(), route);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
debug("Error handling presentation", ex);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
mediaRouter.removeCallback(callback);
|
|
||||||
mediaRouter = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
// The mediaRouter shouldn't exist here, but this is a nice safety check.
|
|
||||||
if (mediaRouter != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaRouter = MediaRouter.getInstance(getActivity());
|
|
||||||
final MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
|
|
||||||
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
|
|
||||||
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
|
|
||||||
.addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastPlayer.MIRROR_RECEIVER_APP_ID))
|
|
||||||
.addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))
|
|
||||||
.build();
|
|
||||||
mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPresentationMode(boolean isPresentationMode) {
|
|
||||||
this.isPresentationMode = isPresentationMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updatePresentation() { /* Overridden in sub-classes. */ }
|
|
||||||
}
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
|
||||||
import org.mozilla.gecko.db.BrowserContract;
|
|
||||||
import org.mozilla.gecko.db.BrowserProvider;
|
|
||||||
import org.mozilla.gecko.home.ImageLoader;
|
|
||||||
import org.mozilla.gecko.icons.storage.MemoryStorage;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.ComponentCallbacks2;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a utility class to keep track of how much memory and disk-space pressure
|
|
||||||
* the system is under. It receives input from GeckoActivity via the onLowMemory() and
|
|
||||||
* onTrimMemory() functions, and also listens for some system intents related to
|
|
||||||
* disk-space notifications. Internally it will track how much memory and disk pressure
|
|
||||||
* the system is under, and perform various actions to help alleviate the pressure.
|
|
||||||
*
|
|
||||||
* Note that since there is no notification for when the system has lots of free memory
|
|
||||||
* again, this class also assumes that, over time, the system will free up memory. This
|
|
||||||
* assumption is implemented using a timer that slowly lowers the internal memory
|
|
||||||
* pressure state if no new low-memory notifications are received.
|
|
||||||
*
|
|
||||||
* Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both
|
|
||||||
* of these classes may be accessed from various threads, and have both been designed to
|
|
||||||
* be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock
|
|
||||||
* is allowed to pick up the MemoryMonitor lock, but not vice-versa.
|
|
||||||
*/
|
|
||||||
class MemoryMonitor extends BroadcastReceiver {
|
|
||||||
private static final String LOGTAG = "GeckoMemoryMonitor";
|
|
||||||
private static final String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP";
|
|
||||||
private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE";
|
|
||||||
|
|
||||||
// Memory pressure levels. Keep these in sync with those in AndroidJavaWrappers.h
|
|
||||||
private static final int MEMORY_PRESSURE_NONE = 0;
|
|
||||||
private static final int MEMORY_PRESSURE_CLEANUP = 1;
|
|
||||||
private static final int MEMORY_PRESSURE_LOW = 2;
|
|
||||||
private static final int MEMORY_PRESSURE_MEDIUM = 3;
|
|
||||||
private static final int MEMORY_PRESSURE_HIGH = 4;
|
|
||||||
|
|
||||||
private static final MemoryMonitor sInstance = new MemoryMonitor();
|
|
||||||
|
|
||||||
static MemoryMonitor getInstance() {
|
|
||||||
return sInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Context mAppContext;
|
|
||||||
private final PressureDecrementer mPressureDecrementer;
|
|
||||||
private int mMemoryPressure; // Synchronized access only.
|
|
||||||
private volatile boolean mStoragePressure; // Accessed via UI thread intent, background runnables.
|
|
||||||
private boolean mInited;
|
|
||||||
|
|
||||||
private MemoryMonitor() {
|
|
||||||
mPressureDecrementer = new PressureDecrementer();
|
|
||||||
mMemoryPressure = MEMORY_PRESSURE_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init(final Context context) {
|
|
||||||
if (mInited) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mAppContext = context.getApplicationContext();
|
|
||||||
IntentFilter filter = new IntentFilter();
|
|
||||||
filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
|
|
||||||
filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
|
|
||||||
filter.addAction(ACTION_MEMORY_DUMP);
|
|
||||||
filter.addAction(ACTION_FORCE_PRESSURE);
|
|
||||||
mAppContext.registerReceiver(this, filter);
|
|
||||||
mInited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onLowMemory() {
|
|
||||||
Log.d(LOGTAG, "onLowMemory() notification received");
|
|
||||||
if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) {
|
|
||||||
// We need to wait on Gecko here, because if we haven't reduced
|
|
||||||
// memory usage enough when we return from this, Android will kill us.
|
|
||||||
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
|
|
||||||
GeckoThread.waitOnGecko();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onTrimMemory(int level) {
|
|
||||||
Log.d(LOGTAG, "onTrimMemory() notification received with level " + level);
|
|
||||||
if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
|
|
||||||
// We seem to get this just by entering the task switcher or hitting the home button.
|
|
||||||
// Seems bogus, because we are the foreground app, or at least not at the end of the LRU list.
|
|
||||||
// Just ignore it, and if there is a real memory pressure event (CRITICAL, MODERATE, etc),
|
|
||||||
// we'll respond appropriately.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (level) {
|
|
||||||
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
|
|
||||||
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
|
|
||||||
// TRIM_MEMORY_MODERATE is the highest level we'll respond to while backgrounded
|
|
||||||
increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
|
|
||||||
break;
|
|
||||||
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
|
|
||||||
increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM);
|
|
||||||
break;
|
|
||||||
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
|
|
||||||
increaseMemoryPressure(MEMORY_PRESSURE_LOW);
|
|
||||||
break;
|
|
||||||
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
|
|
||||||
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
|
|
||||||
increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Log.d(LOGTAG, "Unhandled onTrimMemory() level " + level);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) {
|
|
||||||
Log.d(LOGTAG, "Device storage is low");
|
|
||||||
mStoragePressure = true;
|
|
||||||
ThreadUtils.postToBackgroundThread(new StorageReducer(context));
|
|
||||||
} else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
|
|
||||||
Log.d(LOGTAG, "Device storage is ok");
|
|
||||||
mStoragePressure = false;
|
|
||||||
} else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) {
|
|
||||||
String label = intent.getStringExtra("label");
|
|
||||||
if (label == null) {
|
|
||||||
label = "default";
|
|
||||||
}
|
|
||||||
GeckoAppShell.notifyObservers("Memory:Dump", label);
|
|
||||||
} else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) {
|
|
||||||
increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI(calledFrom = "ui")
|
|
||||||
private static native void dispatchMemoryPressure();
|
|
||||||
|
|
||||||
private boolean increaseMemoryPressure(int level) {
|
|
||||||
int oldLevel;
|
|
||||||
synchronized (this) {
|
|
||||||
// bump up our level if we're not already higher
|
|
||||||
if (mMemoryPressure > level) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
oldLevel = mMemoryPressure;
|
|
||||||
mMemoryPressure = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(LOGTAG, "increasing memory pressure to " + level);
|
|
||||||
|
|
||||||
// since we don't get notifications for when memory pressure is off,
|
|
||||||
// we schedule our own timer to slowly back off the memory pressure level.
|
|
||||||
// note that this will reset the time to next decrement if the decrementer
|
|
||||||
// is already running, which is the desired behaviour because we just got
|
|
||||||
// a new low-mem notification.
|
|
||||||
mPressureDecrementer.start();
|
|
||||||
|
|
||||||
if (oldLevel == level) {
|
|
||||||
// if we're not going to a higher level we probably don't
|
|
||||||
// need to run another round of the same memory reductions
|
|
||||||
// we did on the last memory pressure increase.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO hook in memory-reduction stuff for different levels here
|
|
||||||
if (level >= MEMORY_PRESSURE_MEDIUM) {
|
|
||||||
//Only send medium or higher events because that's all that is used right now
|
|
||||||
if (GeckoThread.isRunning()) {
|
|
||||||
dispatchMemoryPressure();
|
|
||||||
}
|
|
||||||
|
|
||||||
MemoryStorage.get().evictAll();
|
|
||||||
ImageLoader.clearLruCache();
|
|
||||||
LocalBroadcastManager.getInstance(mAppContext)
|
|
||||||
.sendBroadcast(new Intent(BrowserProvider.ACTION_SHRINK_MEMORY));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thread-safe due to mStoragePressure's volatility.
|
|
||||||
*/
|
|
||||||
boolean isUnderStoragePressure() {
|
|
||||||
return mStoragePressure;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean decreaseMemoryPressure() {
|
|
||||||
int newLevel;
|
|
||||||
synchronized (this) {
|
|
||||||
if (mMemoryPressure <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
newLevel = --mMemoryPressure;
|
|
||||||
}
|
|
||||||
Log.d(LOGTAG, "Decreased memory pressure to " + newLevel);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PressureDecrementer implements Runnable {
|
|
||||||
private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
private boolean mPosted;
|
|
||||||
|
|
||||||
synchronized void start() {
|
|
||||||
if (mPosted) {
|
|
||||||
// cancel the old one before scheduling a new one
|
|
||||||
ThreadUtils.getBackgroundHandler().removeCallbacks(this);
|
|
||||||
}
|
|
||||||
ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
|
|
||||||
mPosted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void run() {
|
|
||||||
if (!decreaseMemoryPressure()) {
|
|
||||||
// done decrementing, bail out
|
|
||||||
mPosted = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to keep decrementing
|
|
||||||
ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class StorageReducer implements Runnable {
|
|
||||||
private final Context mContext;
|
|
||||||
private final BrowserDB mDB;
|
|
||||||
|
|
||||||
public StorageReducer(final Context context) {
|
|
||||||
this.mContext = context;
|
|
||||||
// Since this may be called while Fennec is in the background, we don't want to risk accidentally
|
|
||||||
// using the wrong context. If the profile we get is a guest profile, use the default profile instead.
|
|
||||||
GeckoProfile profile = GeckoProfile.get(mContext);
|
|
||||||
if (profile.inGuestMode()) {
|
|
||||||
// If it was the guest profile, switch to the default one.
|
|
||||||
profile = GeckoProfile.get(mContext, GeckoProfile.DEFAULT_PROFILE);
|
|
||||||
}
|
|
||||||
|
|
||||||
mDB = BrowserDB.from(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// this might get run right on startup, if so wait 10 seconds and try again
|
|
||||||
if (!GeckoThread.isRunning()) {
|
|
||||||
ThreadUtils.getBackgroundHandler().postDelayed(this, 10000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!MemoryMonitor.getInstance().isUnderStoragePressure()) {
|
|
||||||
// Pressure is off, so we can abort.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ContentResolver cr = mContext.getContentResolver();
|
|
||||||
mDB.expireHistory(cr, BrowserContract.ExpirePriority.AGGRESSIVE);
|
|
||||||
mDB.removeThumbnails(cr);
|
|
||||||
|
|
||||||
// TODO: drop or shrink disk caches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
public interface MotionEventInterceptor {
|
|
||||||
public boolean onInterceptMotionEvent(View view, MotionEvent event);
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.mozglue.GeckoLoader;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This broadcast receiver receives ACTION_MY_PACKAGE_REPLACED broadcasts and
|
|
||||||
* starts procedures that should run after the APK has been updated.
|
|
||||||
*/
|
|
||||||
public class PackageReplacedReceiver extends BroadcastReceiver {
|
|
||||||
public static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (intent == null || !ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
|
|
||||||
// This is not the broadcast we are looking for.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract Gecko libs to allow them to be loaded from cache on startup.
|
|
||||||
extractGeckoLibs(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void extractGeckoLibs(final Context context) {
|
|
||||||
final String resourcePath = context.getPackageResourcePath();
|
|
||||||
GeckoLoader.loadMozGlue(context);
|
|
||||||
GeckoLoader.extractGeckoLibs(context, resourcePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Presentation;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v7.media.MediaRouter;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Display;
|
|
||||||
import android.view.Surface;
|
|
||||||
import android.view.SurfaceHolder;
|
|
||||||
import android.view.SurfaceView;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A MediaPlayerManager with API 17+ Presentation support.
|
|
||||||
*/
|
|
||||||
@TargetApi(17)
|
|
||||||
public class PresentationMediaPlayerManager extends MediaPlayerManager {
|
|
||||||
|
|
||||||
private static final String LOGTAG = "Gecko" + PresentationMediaPlayerManager.class.getSimpleName();
|
|
||||||
|
|
||||||
private GeckoPresentation presentation;
|
|
||||||
|
|
||||||
public PresentationMediaPlayerManager() {
|
|
||||||
if (!Versions.feature17Plus) {
|
|
||||||
throw new IllegalStateException(PresentationMediaPlayerManager.class.getSimpleName() +
|
|
||||||
" does not support < API 17");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
if (presentation != null) {
|
|
||||||
presentation.dismiss();
|
|
||||||
presentation = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void updatePresentation() {
|
|
||||||
if (mediaRouter == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPresentationMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
|
|
||||||
Display display = route != null ? route.getPresentationDisplay() : null;
|
|
||||||
|
|
||||||
if (display != null) {
|
|
||||||
if ((presentation != null) && (presentation.getDisplay() != display)) {
|
|
||||||
presentation.dismiss();
|
|
||||||
presentation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presentation == null) {
|
|
||||||
final GeckoView geckoView = (GeckoView) getActivity().findViewById(R.id.layer_view);
|
|
||||||
presentation = new GeckoPresentation(getActivity(), display, geckoView);
|
|
||||||
|
|
||||||
try {
|
|
||||||
presentation.show();
|
|
||||||
} catch (WindowManager.InvalidDisplayException ex) {
|
|
||||||
Log.w(LOGTAG, "Couldn't show presentation! Display was removed in "
|
|
||||||
+ "the meantime.", ex);
|
|
||||||
presentation = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (presentation != null) {
|
|
||||||
presentation.dismiss();
|
|
||||||
presentation = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@WrapForJNI(calledFrom = "ui")
|
|
||||||
/* protected */ static native void invalidateAndScheduleComposite(GeckoView geckoView);
|
|
||||||
|
|
||||||
@WrapForJNI(calledFrom = "ui")
|
|
||||||
/* protected */ static native void addPresentationSurface(GeckoView geckoView, Surface surface);
|
|
||||||
|
|
||||||
@WrapForJNI(calledFrom = "ui")
|
|
||||||
/* protected */ static native void removePresentationSurface();
|
|
||||||
|
|
||||||
private static final class GeckoPresentation extends Presentation {
|
|
||||||
private SurfaceView mView;
|
|
||||||
private GeckoView mGeckoView;
|
|
||||||
|
|
||||||
public GeckoPresentation(Context context, Display display, GeckoView geckoView) {
|
|
||||||
super(context, display);
|
|
||||||
|
|
||||||
mGeckoView = geckoView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
mView = new SurfaceView(getContext());
|
|
||||||
setContentView(mView, new ViewGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
|
||||||
mView.getHolder().addCallback(new SurfaceListener(mGeckoView));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class SurfaceListener implements SurfaceHolder.Callback {
|
|
||||||
private GeckoView mGeckoView;
|
|
||||||
|
|
||||||
public SurfaceListener(GeckoView geckoView) {
|
|
||||||
mGeckoView = geckoView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceChanged(SurfaceHolder holder, int format, int width,
|
|
||||||
int height) {
|
|
||||||
// Surface changed so force a composite
|
|
||||||
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
|
|
||||||
invalidateAndScheduleComposite(mGeckoView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
|
||||||
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
|
|
||||||
addPresentationSurface(mGeckoView, holder.getSurface());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
|
||||||
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
|
|
||||||
removePresentationSurface();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
||||||
* vim: ts=4 sw=4 expandtab:
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
||||||
import org.mozilla.gecko.GeckoThread;
|
|
||||||
import org.mozilla.gecko.GeckoView;
|
|
||||||
import org.mozilla.gecko.ScreenManagerHelper;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
|
|
||||||
public class PresentationView extends GeckoView {
|
|
||||||
private static final String LOGTAG = "PresentationView";
|
|
||||||
private static final String presentationViewURI = "chrome://browser/content/PresentationView.xul";
|
|
||||||
|
|
||||||
public PresentationView(Context context, String deviceId, int screenId) {
|
|
||||||
super(context);
|
|
||||||
this.chromeURI = presentationViewURI + "#" + deviceId;
|
|
||||||
this.screenId = screenId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
import org.mozilla.gecko.GeckoAppShell;
|
|
||||||
import org.mozilla.gecko.util.GeckoRequest;
|
|
||||||
import org.mozilla.gecko.util.IOUtils;
|
|
||||||
import org.mozilla.gecko.util.NativeJSObject;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.CancellationSignal;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
|
||||||
import android.print.PrintAttributes;
|
|
||||||
import android.print.PrintDocumentAdapter;
|
|
||||||
import android.print.PrintDocumentAdapter.LayoutResultCallback;
|
|
||||||
import android.print.PrintDocumentAdapter.WriteResultCallback;
|
|
||||||
import android.print.PrintDocumentInfo;
|
|
||||||
import android.print.PrintManager;
|
|
||||||
import android.print.PageRange;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class PrintHelper {
|
|
||||||
private static final String LOGTAG = "GeckoPrintUtils";
|
|
||||||
|
|
||||||
public static void printPDF(final Context context) {
|
|
||||||
GeckoAppShell.sendRequestToGecko(new GeckoRequest("Print:PDF", new JSONObject()) {
|
|
||||||
@Override
|
|
||||||
public void onResponse(NativeJSObject nativeJSObject) {
|
|
||||||
final String filePath = nativeJSObject.getString("file");
|
|
||||||
final String title = nativeJSObject.getString("title");
|
|
||||||
finish(context, filePath, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(NativeJSObject error) {
|
|
||||||
// Gecko didn't respond due to state change, javascript error, etc.
|
|
||||||
Log.d(LOGTAG, "No response from Gecko on request to generate a PDF");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void finish(final Context context, final String filePath, final String title) {
|
|
||||||
PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
|
|
||||||
String jobName = title;
|
|
||||||
|
|
||||||
// The adapter methods are all called on the UI thread by the PrintManager. Put the heavyweight code
|
|
||||||
// in onWrite on the background thread.
|
|
||||||
PrintDocumentAdapter pda = new PrintDocumentAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onWrite(final PageRange[] pages, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal, final WriteResultCallback callback) {
|
|
||||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
InputStream input = null;
|
|
||||||
OutputStream output = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
File pdfFile = new File(filePath);
|
|
||||||
input = new FileInputStream(pdfFile);
|
|
||||||
output = new FileOutputStream(destination.getFileDescriptor());
|
|
||||||
|
|
||||||
byte[] buf = new byte[8192];
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = input.read(buf)) > 0) {
|
|
||||||
output.write(buf, 0, bytesRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES });
|
|
||||||
} catch (FileNotFoundException ee) {
|
|
||||||
Log.d(LOGTAG, "Unable to find the temporary PDF file.");
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.e(LOGTAG, "IOException while transferring temporary PDF file: ", ioe);
|
|
||||||
} finally {
|
|
||||||
IOUtils.safeStreamClose(input);
|
|
||||||
IOUtils.safeStreamClose(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {
|
|
||||||
if (cancellationSignal.isCanceled()) {
|
|
||||||
callback.onLayoutCancelled();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintDocumentInfo pdi = new PrintDocumentInfo.Builder(filePath).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build();
|
|
||||||
callback.onLayoutFinished(pdi, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFinish() {
|
|
||||||
// Remove the temporary file when the printing system is finished.
|
|
||||||
try {
|
|
||||||
File pdfFile = new File(filePath);
|
|
||||||
pdfFile.delete();
|
|
||||||
} catch (NullPointerException npe) {
|
|
||||||
// Silence the exception. We only want to delete a real file. We don't
|
|
||||||
// care if the file doesn't exist.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
printManager.print(jobName, pda, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
|
||||||
|
|
||||||
public class PrivateTab extends Tab {
|
|
||||||
public PrivateTab(Context context, int id, String url, boolean external, int parentId, String title) {
|
|
||||||
super(context, id, url, external, parentId, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void saveThumbnailToDB(final BrowserDB db) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setMetadata(JSONObject metadata) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPrivate() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.db.RemoteClient;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.AlertDialog.Builder;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.app.DialogFragment;
|
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.util.SparseBooleanArray;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A dialog fragment that displays a list of remote clients.
|
|
||||||
* <p>
|
|
||||||
* The dialog allows both single (one tap) and multiple (checkbox) selection.
|
|
||||||
* The dialog's results are communicated via the {@link RemoteClientsListener}
|
|
||||||
* interface. Either the dialog fragment's <i>target fragment</i> (see
|
|
||||||
* {@link Fragment#setTargetFragment(Fragment, int)}), or the containing
|
|
||||||
* <i>activity</i>, must implement that interface. See
|
|
||||||
* {@link #notifyListener(List)} for details.
|
|
||||||
*/
|
|
||||||
public class RemoteClientsDialogFragment extends DialogFragment {
|
|
||||||
private static final String KEY_TITLE = "title";
|
|
||||||
private static final String KEY_CHOICE_MODE = "choice_mode";
|
|
||||||
private static final String KEY_POSITIVE_BUTTON_TEXT = "positive_button_text";
|
|
||||||
private static final String KEY_CLIENTS = "clients";
|
|
||||||
|
|
||||||
public interface RemoteClientsListener {
|
|
||||||
// Always called on the main UI thread.
|
|
||||||
public void onClients(List<RemoteClient> clients);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ChoiceMode {
|
|
||||||
SINGLE,
|
|
||||||
MULTIPLE,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RemoteClientsDialogFragment newInstance(String title, String positiveButtonText, ChoiceMode choiceMode, ArrayList<RemoteClient> clients) {
|
|
||||||
final RemoteClientsDialogFragment dialog = new RemoteClientsDialogFragment();
|
|
||||||
final Bundle args = new Bundle();
|
|
||||||
args.putString(KEY_TITLE, title);
|
|
||||||
args.putString(KEY_POSITIVE_BUTTON_TEXT, positiveButtonText);
|
|
||||||
args.putInt(KEY_CHOICE_MODE, choiceMode.ordinal());
|
|
||||||
args.putParcelableArrayList(KEY_CLIENTS, clients);
|
|
||||||
dialog.setArguments(args);
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RemoteClientsDialogFragment() {
|
|
||||||
// Empty constructor is required for DialogFragment.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
|
|
||||||
GeckoApplication.watchReference(getActivity(), this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void notifyListener(List<RemoteClient> clients) {
|
|
||||||
RemoteClientsListener listener;
|
|
||||||
try {
|
|
||||||
listener = (RemoteClientsListener) getTargetFragment();
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
try {
|
|
||||||
listener = (RemoteClientsListener) getActivity();
|
|
||||||
} catch (ClassCastException f) {
|
|
||||||
throw new ClassCastException(getTargetFragment() + " or " + getActivity()
|
|
||||||
+ " must implement RemoteClientsListener");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listener.onClients(clients);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
final String title = getArguments().getString(KEY_TITLE);
|
|
||||||
final String positiveButtonText = getArguments().getString(KEY_POSITIVE_BUTTON_TEXT);
|
|
||||||
final ChoiceMode choiceMode = ChoiceMode.values()[getArguments().getInt(KEY_CHOICE_MODE)];
|
|
||||||
final ArrayList<RemoteClient> clients = getArguments().getParcelableArrayList(KEY_CLIENTS);
|
|
||||||
|
|
||||||
final Builder builder = new AlertDialog.Builder(getActivity());
|
|
||||||
builder.setTitle(title);
|
|
||||||
|
|
||||||
final String[] clientNames = new String[clients.size()];
|
|
||||||
for (int i = 0; i < clients.size(); i++) {
|
|
||||||
clientNames[i] = clients.get(i).name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (choiceMode == ChoiceMode.MULTIPLE) {
|
|
||||||
builder.setMultiChoiceItems(clientNames, null, null);
|
|
||||||
builder.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialogInterface, int which) {
|
|
||||||
if (which != Dialog.BUTTON_POSITIVE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final AlertDialog dialog = (AlertDialog) dialogInterface;
|
|
||||||
final SparseBooleanArray checkedItemPositions = dialog.getListView().getCheckedItemPositions();
|
|
||||||
final ArrayList<RemoteClient> checked = new ArrayList<RemoteClient>();
|
|
||||||
for (int i = 0; i < clients.size(); i++) {
|
|
||||||
if (checkedItemPositions.get(i)) {
|
|
||||||
checked.add(clients.get(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyListener(checked);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
builder.setItems(clientNames, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int index) {
|
|
||||||
final ArrayList<RemoteClient> checked = new ArrayList<RemoteClient>();
|
|
||||||
checked.add(clients.get(index));
|
|
||||||
notifyListener(checked);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.create();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
||||||
* vim: ts=4 sw=4 expandtab:
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.json.JSONException;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
import org.mozilla.gecko.GeckoAppShell;
|
|
||||||
import org.mozilla.gecko.PresentationView;
|
|
||||||
import org.mozilla.gecko.R;
|
|
||||||
import org.mozilla.gecko.ScreenManagerHelper;
|
|
||||||
import org.mozilla.gecko.annotation.JNITarget;
|
|
||||||
import org.mozilla.gecko.annotation.ReflectionTarget;
|
|
||||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
||||||
import org.mozilla.gecko.gfx.LayerView;
|
|
||||||
import org.mozilla.gecko.util.EventCallback;
|
|
||||||
import org.mozilla.gecko.util.NativeEventListener;
|
|
||||||
import org.mozilla.gecko.util.NativeJSObject;
|
|
||||||
|
|
||||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
|
||||||
import com.google.android.gms.cast.CastPresentation;
|
|
||||||
import com.google.android.gms.cast.CastRemoteDisplayLocalService;
|
|
||||||
import com.google.android.gms.common.ConnectionResult;
|
|
||||||
import com.google.android.gms.common.GooglePlayServicesUtil;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.support.v7.media.MediaControlIntent;
|
|
||||||
import android.support.v7.media.MediaRouteSelector;
|
|
||||||
import android.support.v7.media.MediaRouter.RouteInfo;
|
|
||||||
import android.support.v7.media.MediaRouter;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Display;
|
|
||||||
import android.view.ViewGroup.LayoutParams;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Service to keep the remote display running even when the app goes into the background
|
|
||||||
*/
|
|
||||||
public class RemotePresentationService extends CastRemoteDisplayLocalService {
|
|
||||||
|
|
||||||
private static final String LOGTAG = "RemotePresentationService";
|
|
||||||
private CastPresentation presentation;
|
|
||||||
private String deviceId;
|
|
||||||
private int screenId;
|
|
||||||
|
|
||||||
public void setDeviceId(String deviceId) {
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDeviceId() {
|
|
||||||
return deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreatePresentation(Display display) {
|
|
||||||
createPresentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDismissPresentation() {
|
|
||||||
dismissPresentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dismissPresentation() {
|
|
||||||
if (presentation != null) {
|
|
||||||
presentation.dismiss();
|
|
||||||
presentation = null;
|
|
||||||
ScreenManagerHelper.removeDisplay(screenId);
|
|
||||||
MediaPlayerManager.getInstance().setPresentationMode(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createPresentation() {
|
|
||||||
dismissPresentation();
|
|
||||||
|
|
||||||
MediaPlayerManager.getInstance().setPresentationMode(true);
|
|
||||||
|
|
||||||
DisplayMetrics metrics = new DisplayMetrics();
|
|
||||||
getDisplay().getMetrics(metrics);
|
|
||||||
screenId = ScreenManagerHelper.addDisplay(ScreenManagerHelper.DISPLAY_VIRTUAL,
|
|
||||||
metrics.widthPixels,
|
|
||||||
metrics.heightPixels,
|
|
||||||
metrics.density);
|
|
||||||
|
|
||||||
VirtualPresentation virtualPresentation = new VirtualPresentation(this, getDisplay());
|
|
||||||
virtualPresentation.setDeviceId(deviceId);
|
|
||||||
virtualPresentation.setScreenId(screenId);
|
|
||||||
presentation = (CastPresentation) virtualPresentation;
|
|
||||||
|
|
||||||
try {
|
|
||||||
presentation.show();
|
|
||||||
} catch (WindowManager.InvalidDisplayException ex) {
|
|
||||||
Log.e(LOGTAG, "Unable to show presentation, display was removed.", ex);
|
|
||||||
dismissPresentation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class VirtualPresentation extends CastPresentation {
|
|
||||||
private final String LOGTAG = "VirtualPresentation";
|
|
||||||
private RelativeLayout layout;
|
|
||||||
private PresentationView view;
|
|
||||||
private String deviceId;
|
|
||||||
private int screenId;
|
|
||||||
|
|
||||||
public VirtualPresentation(Context context, Display display) {
|
|
||||||
super(context, display);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
|
|
||||||
public void setScreenId(int screenId) { this.screenId = screenId; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* NOTICE: The context get from getContext() is different to the context
|
|
||||||
* of the application. Presentaion has its own context to get correct
|
|
||||||
* resources.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Create new PresentationView
|
|
||||||
view = new PresentationView(getContext(), deviceId, screenId);
|
|
||||||
view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
|
|
||||||
LayoutParams.MATCH_PARENT));
|
|
||||||
|
|
||||||
// Create new layout to put the GeckoView
|
|
||||||
layout = new RelativeLayout(getContext());
|
|
||||||
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
|
|
||||||
LayoutParams.MATCH_PARENT));
|
|
||||||
layout.addView(view);
|
|
||||||
|
|
||||||
setContentView(layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.Process;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class Restarter extends Service {
|
|
||||||
private static final String LOGTAG = "GeckoRestarter";
|
|
||||||
|
|
||||||
private void doRestart(Intent intent) {
|
|
||||||
final int oldProc = intent.getIntExtra("pid", -1);
|
|
||||||
if (oldProc < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Process.killProcess(oldProc);
|
|
||||||
Log.d(LOGTAG, "Killed " + oldProc);
|
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (final InterruptedException e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
final Intent restartIntent = (Intent)intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
|
||||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
.putExtra("didRestart", true)
|
|
||||||
.setClassName(getApplicationContext(),
|
|
||||||
AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
|
||||||
startActivity(restartIntent);
|
|
||||||
Log.d(LOGTAG, "Launched " + restartIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
doRestart(intent);
|
|
||||||
stopSelf(startId);
|
|
||||||
return Service.START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
||||||
* vim: ts=4 sw=4 expandtab:
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
||||||
|
|
||||||
class ScreenManagerHelper {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The following display types use the same definition in nsIScreen.idl
|
|
||||||
*/
|
|
||||||
final static int DISPLAY_PRIMARY = 0; // primary screen
|
|
||||||
final static int DISPLAY_EXTERNAL = 1; // wired displays, such as HDMI, DisplayPort, etc.
|
|
||||||
final static int DISPLAY_VIRTUAL = 2; // wireless displays, such as Chromecast, WiFi-Display, etc.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new nsScreen when a new display in Android is available.
|
|
||||||
*
|
|
||||||
* @param displayType the display type of the nsScreen would be added
|
|
||||||
* @param width the width of the new nsScreen
|
|
||||||
* @param height the height of the new nsScreen
|
|
||||||
* @param density the density of the new nsScreen
|
|
||||||
*
|
|
||||||
* @return return the ID of the added nsScreen
|
|
||||||
*/
|
|
||||||
@WrapForJNI
|
|
||||||
public native static int addDisplay(int displayType,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
float density);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the nsScreen by the specific screen ID.
|
|
||||||
*
|
|
||||||
* @param screenId the ID of the screen would be removed.
|
|
||||||
*/
|
|
||||||
@WrapForJNI
|
|
||||||
public native static void removeDisplay(int screenId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
|
|
||||||
|
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
|
||||||
import org.mozilla.gecko.permissions.Permissions;
|
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.ContentObserver;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class ScreenshotObserver {
|
|
||||||
private static final String LOGTAG = "GeckoScreenshotObserver";
|
|
||||||
public Context context;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for screenshot changes.
|
|
||||||
*/
|
|
||||||
public interface OnScreenshotListener {
|
|
||||||
/**
|
|
||||||
* This callback is executed on the UI thread.
|
|
||||||
*/
|
|
||||||
public void onScreenshotTaken(String data, String title);
|
|
||||||
}
|
|
||||||
|
|
||||||
private OnScreenshotListener listener;
|
|
||||||
|
|
||||||
public ScreenshotObserver() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(Context context, OnScreenshotListener listener) {
|
|
||||||
this.context = context;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MediaObserver mediaObserver;
|
|
||||||
private String[] mediaProjections = new String[] {
|
|
||||||
MediaStore.Images.ImageColumns.DATA,
|
|
||||||
MediaStore.Images.ImageColumns.DISPLAY_NAME,
|
|
||||||
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
|
|
||||||
MediaStore.Images.ImageColumns.DATE_TAKEN,
|
|
||||||
MediaStore.Images.ImageColumns.TITLE
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start ScreenshotObserver if this device is supported and all required runtime permissions
|
|
||||||
* have been granted by the user. Calling this method will not prompt for permissions.
|
|
||||||
*/
|
|
||||||
public void start() {
|
|
||||||
Permissions.from(context)
|
|
||||||
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
.doNotPrompt()
|
|
||||||
.run(startObserverRunnable());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Runnable startObserverRunnable() {
|
|
||||||
return new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
if (mediaObserver == null) {
|
|
||||||
mediaObserver = new MediaObserver();
|
|
||||||
context.getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mediaObserver);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "Failure to start watching media: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (mediaObserver == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
context.getContentResolver().unregisterContentObserver(mediaObserver);
|
|
||||||
mediaObserver = null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "Failure to stop watching media: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onMediaChange(final Uri uri) {
|
|
||||||
// Make sure we are on not on the main thread.
|
|
||||||
final ContentResolver cr = context.getContentResolver();
|
|
||||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Find the most recent image added to the MediaStore and see if it's a screenshot.
|
|
||||||
final Cursor cursor = cr.query(uri, mediaProjections, null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " DESC LIMIT 1");
|
|
||||||
try {
|
|
||||||
if (cursor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String data = cursor.getString(0);
|
|
||||||
Log.i(LOGTAG, "data: " + data);
|
|
||||||
String display = cursor.getString(1);
|
|
||||||
Log.i(LOGTAG, "display: " + display);
|
|
||||||
String album = cursor.getString(2);
|
|
||||||
Log.i(LOGTAG, "album: " + album);
|
|
||||||
long date = cursor.getLong(3);
|
|
||||||
String title = cursor.getString(4);
|
|
||||||
Log.i(LOGTAG, "title: " + title);
|
|
||||||
if (album != null && album.toLowerCase().contains("screenshot")) {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onScreenshotTaken(data, title);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(LOGTAG, "Failure to process media change: ", e);
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MediaObserver extends ContentObserver {
|
|
||||||
public MediaObserver() {
|
|
||||||
super(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onChange(boolean selfChange) {
|
|
||||||
super.onChange(selfChange);
|
|
||||||
onMediaChange(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
||||||
* ***** BEGIN LICENSE BLOCK *****
|
|
||||||
*
|
|
||||||
* 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/.
|
|
||||||
*
|
|
||||||
* ***** END LICENSE BLOCK ***** */
|
|
||||||
|
|
||||||
package org.mozilla.gecko;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public abstract class SessionParser {
|
|
||||||
private static final String LOGTAG = "GeckoSessionParser";
|
|
||||||
|
|
||||||
public class SessionTab {
|
|
||||||
final private String mTitle;
|
|
||||||
final private String mUrl;
|
|
||||||
final private JSONObject mTabObject;
|
|
||||||
private boolean mIsSelected;
|
|
||||||
|
|
||||||
private SessionTab(String title, String url, boolean isSelected, JSONObject tabObject) {
|
|
||||||
mTitle = title;
|
|
||||||
mUrl = url;
|
|
||||||
mIsSelected = isSelected;
|
|
||||||
mTabObject = tabObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return mTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return mUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSelected() {
|
|
||||||
return mIsSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JSONObject getTabObject() {
|
|
||||||
return mTabObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this tab pointing to about:home and does not contain any other history?
|
|
||||||
*/
|
|
||||||
public boolean isAboutHomeWithoutHistory() {
|
|
||||||
JSONArray entries = mTabObject.optJSONArray("entries");
|
|
||||||
return entries != null && entries.length() == 1 && AboutPages.isAboutHome(mUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
abstract public void onTabRead(SessionTab tab);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder method that must be overloaded to handle closedTabs while parsing session data.
|
|
||||||
*
|
|
||||||
* @param closedTabs, JSONArray of recently closed tab entries.
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
|
||||||
public void onClosedTabsRead(final JSONArray closedTabs) throws JSONException {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the provided session store data and calls onTabRead for each tab that has been found.
|
|
||||||
*
|
|
||||||
* @param sessionStrings One or more strings containing session store data.
|
|
||||||
* @return False if any of the session strings provided didn't contain valid session store data.
|
|
||||||
*/
|
|
||||||
public boolean parse(String... sessionStrings) {
|
|
||||||
final LinkedList<SessionTab> sessionTabs = new LinkedList<SessionTab>();
|
|
||||||
int totalCount = 0;
|
|
||||||
int selectedIndex = -1;
|
|
||||||
try {
|
|
||||||
for (String sessionString : sessionStrings) {
|
|
||||||
final JSONArray windowsArray = new JSONObject(sessionString).getJSONArray("windows");
|
|
||||||
if (windowsArray.length() == 0) {
|
|
||||||
// Session json can be empty if the user has opted out of session restore.
|
|
||||||
Log.d(LOGTAG, "Session restore file is empty, no session entries found.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final JSONObject window = windowsArray.getJSONObject(0);
|
|
||||||
final JSONArray tabs = window.getJSONArray("tabs");
|
|
||||||
final int optSelected = window.optInt("selected", -1);
|
|
||||||
final JSONArray closedTabs = window.optJSONArray("closedTabs");
|
|
||||||
if (closedTabs != null) {
|
|
||||||
onClosedTabsRead(closedTabs);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < tabs.length(); i++) {
|
|
||||||
final JSONObject tab = tabs.getJSONObject(i);
|
|
||||||
final int index = tab.getInt("index");
|
|
||||||
final JSONArray entries = tab.getJSONArray("entries");
|
|
||||||
if (index < 1 || entries.length() < index) {
|
|
||||||
Log.w(LOGTAG, "Session entries and index don't agree.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final JSONObject entry = entries.getJSONObject(index - 1);
|
|
||||||
final String url = entry.getString("url");
|
|
||||||
|
|
||||||
String title = entry.optString("title");
|
|
||||||
if (title.length() == 0) {
|
|
||||||
title = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalCount++;
|
|
||||||
boolean selected = false;
|
|
||||||
if (optSelected == i + 1) {
|
|
||||||
selected = true;
|
|
||||||
selectedIndex = totalCount;
|
|
||||||
}
|
|
||||||
sessionTabs.add(new SessionTab(title, url, selected, tab));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(LOGTAG, "JSON error", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no selected index was found, select the first tab.
|
|
||||||
if (selectedIndex == -1 && sessionTabs.size() > 0) {
|
|
||||||
sessionTabs.getFirst().mIsSelected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (SessionTab tab : sessionTabs) {
|
|
||||||
onTabRead(tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue