This commit is contained in:
Wes Kocher 2015-02-23 16:00:36 -08:00
Родитель e522eacb94 7baddf9574
Коммит a2295b58c5
24 изменённых файлов: 1229 добавлений и 395 удалений

Просмотреть файл

@ -915,16 +915,46 @@ html, .fx-embedded, #main,
.standalone .room-conversation-wrapper .room-inner-info-area { .standalone .room-conversation-wrapper .room-inner-info-area {
position: absolute; position: absolute;
top: calc(50% - 1em); /* `top` is chosen to vertically position the area near the center
left: 0; of the media element. */
right: 25%; top: calc(50% - 140px);
left: 25%;
z-index: 1000; z-index: 1000;
margin: 0 auto; /* `width` here is specified by the design spec. */
padding: 0 1rem; width: 250px;
width: 50%;
color: #fff; color: #fff;
font-weight: bold; box-sizing: content-box;
font-size: 1.1em; }
.standalone .prompt-media-message {
padding-top: 136px; /* Fallback for browsers that don't support calc() */
/* 122px is 2x the intrinsic height of the background-image, and
1rem puts one line of margin between the background-image and
supporting text. */
padding-top: calc(122px + 1rem);
color: #000;
background-color: #fff;
background-image: url("../../img/gum-others.svg");
background-position: top center;
/* The background-image is scaled up at 2x the instrinsic size
(witdh & height) to make it easier to see. */
background-size: 202px 122px;
background-repeat: no-repeat;
border: 1rem #fff solid;
box-shadow: 0 0 5px #000;
margin: 0;
}
.standalone .prompt-media-message.chrome {
background-image: url("../../img/gum-chrome.svg");
}
.standalone .prompt-media-message.firefox {
background-image: url("../../img/gum-firefox.svg");
}
.standalone .prompt-media-message.opera {
background-image: url("../../img/gum-opera.svg");
} }
.standalone .room-conversation-wrapper .room-inner-info-area button { .standalone .room-conversation-wrapper .room-inner-info-area button {

Просмотреть файл

@ -84,8 +84,13 @@ loop.shared.utils = (function(mozL10n) {
return !!localStorage.getItem(prefName); return !!localStorage.getItem(prefName);
} }
function isChrome(platform) {
return platform.toLowerCase().indexOf('chrome') > -1 ||
platform.toLowerCase().indexOf('chromium') > -1;
}
function isFirefox(platform) { function isFirefox(platform) {
return platform.indexOf("Firefox") !== -1; return platform.toLowerCase().indexOf("firefox") !== -1;
} }
function isFirefoxOS(platform) { function isFirefoxOS(platform) {
@ -97,6 +102,11 @@ loop.shared.utils = (function(mozL10n) {
return !!window.MozActivity && /mobi/i.test(platform); return !!window.MozActivity && /mobi/i.test(platform);
} }
function isOpera(platform) {
return platform.toLowerCase().indexOf('opera') > -1 ||
platform.toLowerCase().indexOf('opr') > -1;
}
/** /**
* Helper to get the platform if it is unsupported. * Helper to get the platform if it is unsupported.
* *
@ -167,8 +177,10 @@ loop.shared.utils = (function(mozL10n) {
composeCallUrlEmail: composeCallUrlEmail, composeCallUrlEmail: composeCallUrlEmail,
formatDate: formatDate, formatDate: formatDate,
getBoolPreference: getBoolPreference, getBoolPreference: getBoolPreference,
isChrome: isChrome,
isFirefox: isFirefox, isFirefox: isFirefox,
isFirefoxOS: isFirefoxOS, isFirefoxOS: isFirefoxOS,
isOpera: isOpera,
getUnsupportedPlatform: getUnsupportedPlatform, getUnsupportedPlatform: getUnsupportedPlatform,
locationData: locationData locationData: locationData
}; };

Просмотреть файл

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="102px" height="62px" viewBox="0 0 102 62" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(0.500000, 0.000000)">
<rect stroke="#ABE6F8" fill="#FFFFFF" x="0.5" y="1" width="100" height="60"/>
<path d="M36.4003,3 L39.02,9.66666667 L34.8325,9.66666667 L5.6875,9.66666667 L1.5,9.66666667 L4.1197,3 L36.4003,3 Z" fill="#AAE5F9"/>
<rect fill="#AAE5F9" x="0.5" y="9" width="100" height="10"/>
<path d="M21.004515,11.3333333 C19.6213095,11.3333333 18.5,12.4429954 18.5,13.8333333 L18.5,13.8333333 C18.5,15.2140452 19.6250227,16.3333333 21.0055946,16.3333333 L40.8816964,16.3333333 L90.002759,16.3333333 C91.3819471,16.3333333 92.5,15.2236713 92.5,13.8333333 L92.5,13.8333333 C92.5,12.4526215 91.3819191,11.3333333 89.995485,11.3333333 L21.004515,11.3333333 Z" fill="#FFFFFF"/>
<rect stroke="#ABE6F8" x="0" y="0" width="101" height="62"/>
<g transform="translate(0.000000, 18.000000)">
<rect stroke="#92CB3E" stroke-width="2" fill="#FFFFFF" x="0.5" y="0" width="100" height="10"/>
<path d="M89.5096131,3 C88.3997344,3 87.5,3.88772964 87.5,5 L87.5,5 C87.5,6.1045695 88.3933569,7 89.4971942,7 L88.5360952,7 C89.6426425,7 91.4357226,7 92.5331547,7 L95.5533191,7 C96.6560555,7 97.55,6.11227036 97.55,5 L97.55,5 C97.55,3.8954305 96.6581295,3 95.5403869,3 L89.5096131,3 Z" stroke="#92CB3E" fill="#FFFFFF"/>
<path d="M78.5096131,3 C77.3997344,3 76.5,3.88772964 76.5,5 L76.5,5 C76.5,6.1045695 77.3933569,7 78.4971942,7 L77.5360952,7 C78.6426425,7 80.4357226,7 81.5331547,7 L84.5533191,7 C85.6560555,7 86.55,6.11227036 86.55,5 L86.55,5 C86.55,3.8954305 85.6581295,3 84.5403869,3 L78.5096131,3 Z" stroke="#92CB3E" fill="#FFFFFF"/>
<path d="M8.5,3.69642857 L8.5,6.73214286 C8.5,6.81026825 8.46372804,6.86514121 8.39118304,6.89676339 C8.36700137,6.90606403 8.34375011,6.91071429 8.32142857,6.91071429 C8.27120511,6.91071429 8.22935285,6.89304333 8.19587054,6.85770089 L7.07142857,5.73325893 L7.07142857,6.19642857 C7.07142857,6.41778384 6.99283933,6.60704907 6.83565848,6.76422991 C6.67847764,6.92141076 6.48921242,7 6.26785714,7 L4.30357143,7 C4.08221616,7 3.89295093,6.92141076 3.73577009,6.76422991 C3.57858924,6.60704907 3.5,6.41778384 3.5,6.19642857 L3.5,4.23214286 C3.5,4.01078758 3.57858924,3.82152236 3.73577009,3.66434152 C3.89295093,3.50716067 4.08221616,3.42857143 4.30357143,3.42857143 L6.26785714,3.42857143 C6.48921242,3.42857143 6.67847764,3.50716067 6.83565848,3.66434152 C6.99283933,3.82152236 7.07142857,4.01078758 7.07142857,4.23214286 L7.07142857,4.69252232 L8.19587054,3.57087054 C8.22935285,3.5355281 8.27120511,3.51785714 8.32142857,3.51785714 C8.34375011,3.51785714 8.36700137,3.52250739 8.39118304,3.53180804 C8.46372804,3.56343022 8.5,3.61830318 8.5,3.69642857 L8.5,3.69642857 Z" fill="#00AF84"/>
</g>
<path d="M97.9285714,14.5714286 L97.9285714,14.8571429 C97.9285714,14.8958335 97.9144347,14.9293153 97.8861607,14.9575893 C97.8578868,14.9858632 97.824405,15 97.7857143,15 L94.6428571,15 C94.6041665,15 94.5706847,14.9858632 94.5424107,14.9575893 C94.5141368,14.9293153 94.5,14.8958335 94.5,14.8571429 L94.5,14.5714286 C94.5,14.5327379 94.5141368,14.4992561 94.5424107,14.4709821 C94.5706847,14.4427082 94.6041665,14.4285714 94.6428571,14.4285714 L97.7857143,14.4285714 C97.824405,14.4285714 97.8578868,14.4427082 97.8861607,14.4709821 C97.9144347,14.4992561 97.9285714,14.5327379 97.9285714,14.5714286 L97.9285714,14.5714286 Z M97.9285714,13.4285714 L97.9285714,13.7142857 C97.9285714,13.7529764 97.9144347,13.7864582 97.8861607,13.8147321 C97.8578868,13.8430061 97.824405,13.8571429 97.7857143,13.8571429 L94.6428571,13.8571429 C94.6041665,13.8571429 94.5706847,13.8430061 94.5424107,13.8147321 C94.5141368,13.7864582 94.5,13.7529764 94.5,13.7142857 L94.5,13.4285714 C94.5,13.3898808 94.5141368,13.356399 94.5424107,13.328125 C94.5706847,13.299851 94.6041665,13.2857143 94.6428571,13.2857143 L97.7857143,13.2857143 C97.824405,13.2857143 97.8578868,13.299851 97.8861607,13.328125 C97.9144347,13.356399 97.9285714,13.3898808 97.9285714,13.4285714 L97.9285714,13.4285714 Z M97.9285714,12.2857143 L97.9285714,12.5714286 C97.9285714,12.6101192 97.9144347,12.643601 97.8861607,12.671875 C97.8578868,12.700149 97.824405,12.7142857 97.7857143,12.7142857 L94.6428571,12.7142857 C94.6041665,12.7142857 94.5706847,12.700149 94.5424107,12.671875 C94.5141368,12.643601 94.5,12.6101192 94.5,12.5714286 L94.5,12.2857143 C94.5,12.2470236 94.5141368,12.2135418 94.5424107,12.1852679 C94.5706847,12.1569939 94.6041665,12.1428571 94.6428571,12.1428571 L97.7857143,12.1428571 C97.824405,12.1428571 97.8578868,12.1569939 97.8861607,12.1852679 C97.9144347,12.2135418 97.9285714,12.2470236 97.9285714,12.2857143 L97.9285714,12.2857143 Z" fill="#FFFFFF"/>
<path d="M5.92857143,13.5714286 L5.92857143,13.8571429 C5.92857143,13.9360123 5.90439012,14.0033479 5.85602679,14.0591518 C5.80766345,14.1149556 5.74479205,14.1428571 5.66741071,14.1428571 L4.09598214,14.1428571 L4.75,14.7991071 C4.8065479,14.8526788 4.83482143,14.9196425 4.83482143,15 C4.83482143,15.0803575 4.8065479,15.1473212 4.75,15.2008929 L4.58258929,15.3705357 C4.52752949,15.4255955 4.46056587,15.453125 4.38169643,15.453125 C4.30431509,15.453125 4.23660743,15.4255955 4.17857143,15.3705357 L2.72544643,13.9151786 C2.67038663,13.8601188 2.64285714,13.7931552 2.64285714,13.7142857 C2.64285714,13.6369044 2.67038663,13.5691967 2.72544643,13.5111607 L4.17857143,12.0602679 C4.23511933,12.00372 4.30282699,11.9754464 4.38169643,11.9754464 C4.45907777,11.9754464 4.52604138,12.00372 4.58258929,12.0602679 L4.75,12.2254464 C4.8065479,12.2819943 4.83482143,12.349702 4.83482143,12.4285714 C4.83482143,12.5074409 4.8065479,12.5751485 4.75,12.6316964 L4.09598214,13.2857143 L5.66741071,13.2857143 C5.74479205,13.2857143 5.80766345,13.3136158 5.85602679,13.3694196 C5.90439012,13.4252235 5.92857143,13.4925591 5.92857143,13.5714286 L5.92857143,13.5714286 Z" fill="#FFFFFF"/>
<path d="M10.9285714,13.5714286 L10.9285714,13.8571429 C10.9285714,13.9360123 10.9043901,14.0033479 10.8560268,14.0591518 C10.8076634,14.1149556 10.7447921,14.1428571 10.6674107,14.1428571 L9.09598214,14.1428571 L9.75,14.7991071 C9.8065479,14.8526788 9.83482143,14.9196425 9.83482143,15 C9.83482143,15.0803575 9.8065479,15.1473212 9.75,15.2008929 L9.58258929,15.3705357 C9.52752949,15.4255955 9.46056587,15.453125 9.38169643,15.453125 C9.30431509,15.453125 9.23660743,15.4255955 9.17857143,15.3705357 L7.72544643,13.9151786 C7.67038663,13.8601188 7.64285714,13.7931552 7.64285714,13.7142857 C7.64285714,13.6369044 7.67038663,13.5691967 7.72544643,13.5111607 L9.17857143,12.0602679 C9.23511933,12.00372 9.30282699,11.9754464 9.38169643,11.9754464 C9.45907777,11.9754464 9.52604138,12.00372 9.58258929,12.0602679 L9.75,12.2254464 C9.8065479,12.2819943 9.83482143,12.349702 9.83482143,12.4285714 C9.83482143,12.5074409 9.8065479,12.5751485 9.75,12.6316964 L9.09598214,13.2857143 L10.6674107,13.2857143 C10.7447921,13.2857143 10.8076634,13.3136158 10.8560268,13.3694196 C10.9043901,13.4252235 10.9285714,13.4925591 10.9285714,13.5714286 L10.9285714,13.5714286 Z" fill="#FFFFFF" transform="translate(9.285714, 13.714286) scale(-1, 1) translate(-9.285714, -13.714286) "/>
<path d="M16.9285714,12.1428571 L16.9285714,13.1428571 C16.9285714,13.1815478 16.9144347,13.2150296 16.8861607,13.2433036 C16.8578868,13.2715775 16.824405,13.2857143 16.7857143,13.2857143 L15.7857143,13.2857143 C15.723214,13.2857143 15.6793156,13.2559527 15.6540179,13.1964286 C15.6287201,13.1383926 15.6391367,13.0870538 15.6852679,13.0424107 L15.9933036,12.734375 C15.7730644,12.5305049 15.5133944,12.4285714 15.2142857,12.4285714 C15.059523,12.4285714 14.9118311,12.4587051 14.7712054,12.5189732 C14.6305797,12.5792414 14.5089291,12.6607138 14.40625,12.7633929 C14.3035709,12.8660719 14.2220985,12.9877225 14.1618304,13.1283482 C14.1015622,13.2689739 14.0714286,13.4166659 14.0714286,13.5714286 C14.0714286,13.7261912 14.1015622,13.8738832 14.1618304,14.0145089 C14.2220985,14.1551346 14.3035709,14.2767852 14.40625,14.3794643 C14.5089291,14.4821434 14.6305797,14.5636158 14.7712054,14.6238839 C14.9118311,14.6841521 15.059523,14.7142857 15.2142857,14.7142857 C15.3913699,14.7142857 15.558779,14.6755956 15.7165179,14.5982143 C15.8742567,14.5208329 16.0074399,14.411459 16.1160714,14.2700893 C16.1264881,14.2552083 16.1436011,14.2462798 16.1674107,14.2433036 C16.1882442,14.2433036 16.2068452,14.2499999 16.2232143,14.2633929 L16.5290179,14.5714286 C16.5424108,14.5833334 16.5494792,14.5985862 16.5502232,14.6171875 C16.5509673,14.6357888 16.545387,14.6525297 16.5334821,14.6674107 C16.371279,14.8638403 16.1748523,15.0159965 15.9441964,15.1238839 C15.7135405,15.2317714 15.4702394,15.2857143 15.2142857,15.2857143 C14.9821417,15.2857143 14.7604177,15.2403278 14.5491071,15.1495536 C14.3377966,15.0587793 14.1555067,14.9367567 14.0022321,14.7834821 C13.8489576,14.6302076 13.726935,14.4479177 13.6361607,14.2366071 C13.5453865,14.0252966 13.5,13.8035726 13.5,13.5714286 C13.5,13.3392846 13.5453865,13.1175606 13.6361607,12.90625 C13.726935,12.6949394 13.8489576,12.5126496 14.0022321,12.359375 C14.1555067,12.2061004 14.3377966,12.0840778 14.5491071,11.9933036 C14.7604177,11.9025293 14.9821417,11.8571429 15.2142857,11.8571429 C15.4330368,11.8571429 15.6447162,11.8984371 15.8493304,11.9810268 C16.0539445,12.0636165 16.2358623,12.1800588 16.3950893,12.3303571 L16.6852679,12.0424107 C16.7284228,11.9962795 16.7805056,11.985863 16.8415179,12.0111607 C16.8995539,12.0364585 16.9285714,12.0803568 16.9285714,12.1428571 L16.9285714,12.1428571 Z" fill="#FFFFFF"/>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 9.5 KiB

Просмотреть файл

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="102px" height="62px" viewBox="0 0 102 62" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M39.565,8.00739557 C39.4896406,8.00249919 39.4115419,8 39.3306224,8 C35.3657519,8 37.8823812,2 33.0112546,2 L7.18295591,2 C2.59740823,2 4.61972552,7.31710024 1.565,7.94055011 L1.565,7.94055011 L1.565,14 L15.5658248,14 L39.565,14 L39.565,8.00739557 Z" id="path-1"/>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(0.500000, 0.000000)">
<g transform="translate(0.000000, 1.000000)">
<rect stroke="#ABE6F8" fill="#FFFFFF" x="0.565" y="0" width="100" height="60"/>
<g>
<use fill="#AAE5F9" fill-rule="evenodd" xlink:href="#path-1"/>
<use fill="none" xlink:href="#path-1"/>
<use fill="none" xlink:href="#path-1"/>
</g>
<rect fill="#ABE6F8" x="0.565" y="7" width="99.685" height="10"/>
<path d="M8.07491535,9.33333333 C6.68872738,9.33333333 5.565,10.4429954 5.565,11.8333333 L5.565,11.8333333 C5.565,13.2140452 6.68484687,14.3333333 8.05891597,14.3333333 L31.8786161,14.3333333 L90.0705787,14.3333333 C91.4482095,14.3333333 92.565,13.2236713 92.565,11.8333333 L92.565,11.8333333 C92.565,10.4526215 91.4491428,9.33333333 90.0550846,9.33333333 L8.07491535,9.33333333 Z" fill="#FFFFFF"/>
<ellipse stroke="#FFFFFF" fill="#AAE5F9" cx="5.565" cy="12" rx="3" ry="3"/>
<path d="M16.2240909,16.6938776 L44.5630004,16.6938776 C45.6668839,16.6938776 46.565,17.5893929 46.565,18.6940672 L46.565,44.9998104 C46.565,46.1094081 45.6686743,47 44.5630004,47 L5.56699958,47 C4.46311611,47 3.565,46.1044846 3.565,44.9998104 L3.565,18.6940672 C3.565,17.5844694 4.46132574,16.6938776 5.56699958,16.6938776 L8.40590909,16.6938776 L12.315,14 L16.2240909,16.6938776 Z" stroke="#92CB3E" stroke-width="2" fill="#FFFFFF"/>
<path d="M25.0575753,38 C23.680964,38 22.565,39.1096621 22.565,40.5 L22.565,40.5 C22.565,41.8807119 23.6941328,43 25.0722854,43 L26.115167,43 C27.4952935,43 29.7367265,43 31.1231981,43 L40.0643408,43 C41.4454167,43 42.565,41.8903379 42.565,40.5 L42.565,40.5 C42.565,39.1192881 41.4468578,38 40.0724247,38 L25.0575753,38 Z" stroke="#92CB3E" fill="#FFFFFF"/>
<path d="M17.565,22.3928571 L17.565,28.4642857 C17.565,28.6205365 17.4924561,28.7302824 17.3473661,28.7935268 C17.2990027,28.8121281 17.2525002,28.8214286 17.2078571,28.8214286 C17.1074102,28.8214286 17.0237057,28.7860867 16.9567411,28.7154018 L14.7078571,26.4665179 L14.7078571,27.3928571 C14.7078571,27.8355677 14.5506787,28.2140981 14.236317,28.5284598 C13.9219553,28.8428215 13.5434248,29 13.1007143,29 L9.17214286,29 C8.72943231,29 8.35090187,28.8428215 8.03654018,28.5284598 C7.72217849,28.2140981 7.565,27.8355677 7.565,27.3928571 L7.565,23.4642857 C7.565,23.0215752 7.72217849,22.6430447 8.03654018,22.328683 C8.35090187,22.0143213 8.72943231,21.8571429 9.17214286,21.8571429 L13.1007143,21.8571429 C13.5434248,21.8571429 13.9219553,22.0143213 14.236317,22.328683 C14.5506787,22.6430447 14.7078571,23.0215752 14.7078571,23.4642857 L14.7078571,24.3850446 L16.9567411,22.1417411 C17.0237057,22.0710562 17.1074102,22.0357143 17.2078571,22.0357143 C17.2525002,22.0357143 17.2990027,22.0450148 17.3473661,22.0636161 C17.4924561,22.1268604 17.565,22.2366064 17.565,22.3928571 L17.565,22.3928571 Z" fill="#00AF84"/>
<path d="M13.565,11.0178571 L13.565,12.8392857 C13.565,12.8861609 13.5432368,12.9190847 13.4997098,12.938058 C13.4852008,12.9436384 13.4712501,12.9464286 13.4578571,12.9464286 C13.4277231,12.9464286 13.4026117,12.935826 13.3825223,12.9146205 L12.7078571,12.2399554 L12.7078571,12.5178571 C12.7078571,12.6506703 12.6607036,12.7642294 12.5663951,12.8585379 C12.4720866,12.9528465 12.3585274,13 12.2257143,13 L11.0471429,13 C10.9143297,13 10.8007706,12.9528465 10.7064621,12.8585379 C10.6121535,12.7642294 10.565,12.6506703 10.565,12.5178571 L10.565,11.3392857 C10.565,11.2064726 10.6121535,11.0929134 10.7064621,10.9986049 C10.8007706,10.9042964 10.9143297,10.8571429 11.0471429,10.8571429 L12.2257143,10.8571429 C12.3585274,10.8571429 12.4720866,10.9042964 12.5663951,10.9986049 C12.6607036,11.0929134 12.7078571,11.2064726 12.7078571,11.3392857 L12.7078571,11.6155134 L13.3825223,10.9425223 C13.4026117,10.9213169 13.4277231,10.9107143 13.4578571,10.9107143 C13.4712501,10.9107143 13.4852008,10.9135044 13.4997098,10.9190848 C13.5432368,10.9380581 13.565,10.9709819 13.565,11.0178571 L13.565,11.0178571 Z" fill="#00AF84"/>
<path d="M6.86418806,12.1429618 C6.86418806,12.2021139 6.84605208,12.2526156 6.80977958,12.2944685 C6.77350707,12.3363214 6.72635353,12.3572475 6.66831752,12.3572475 L5.48974609,12.3572475 L5.98025949,12.849435 C6.02267041,12.8896138 6.04387556,12.9398365 6.04387556,13.0001046 C6.04387556,13.0603728 6.02267041,13.1105955 5.98025949,13.1507743 L5.85470145,13.2780064 C5.8134066,13.3193013 5.76318389,13.3399484 5.70403181,13.3399484 C5.6459958,13.3399484 5.59521506,13.3193013 5.55168806,13.2780064 L4.46184431,12.1864886 C4.42054946,12.1451937 4.39990234,12.094971 4.39990234,12.0358189 C4.39990234,11.9777829 4.42054946,11.9270022 4.46184431,11.8834752 L5.55168806,10.7953055 C5.59409898,10.7528946 5.64487973,10.7316895 5.70403181,10.7316895 C5.76206781,10.7316895 5.81229052,10.7528946 5.85470145,10.7953055 L5.98025949,10.9191895 C6.02267041,10.9616004 6.04387556,11.0123811 6.04387556,11.0715332 C6.04387556,11.1306853 6.02267041,11.181466 5.98025949,11.223877 L5.48974609,11.7143903 L6.66831752,11.7143903 C6.72635353,11.7143903 6.77350707,11.7353165 6.80977958,11.7771694 C6.84605208,11.8190223 6.86418806,11.869524 6.86418806,11.9286761 L6.86418806,12.1429618 Z" fill="#FFFFFF"/>
<path d="M97.1364286,12.6785714 L97.1364286,12.8928571 C97.1364286,12.9218751 97.125826,12.9469865 97.1046205,12.968192 C97.0834151,12.9893974 97.0583037,13 97.0292857,13 L94.6721429,13 C94.6431249,13 94.6180135,12.9893974 94.596808,12.968192 C94.5756026,12.9469865 94.565,12.9218751 94.565,12.8928571 L94.565,12.6785714 C94.565,12.6495534 94.5756026,12.6244421 94.596808,12.6032366 C94.6180135,12.5820311 94.6431249,12.5714286 94.6721429,12.5714286 L97.0292857,12.5714286 C97.0583037,12.5714286 97.0834151,12.5820311 97.1046205,12.6032366 C97.125826,12.6244421 97.1364286,12.6495534 97.1364286,12.6785714 L97.1364286,12.6785714 Z M97.1364286,11.8214286 L97.1364286,12.0357143 C97.1364286,12.0647323 97.125826,12.0898436 97.1046205,12.1110491 C97.0834151,12.1322546 97.0583037,12.1428571 97.0292857,12.1428571 L94.6721429,12.1428571 C94.6431249,12.1428571 94.6180135,12.1322546 94.596808,12.1110491 C94.5756026,12.0898436 94.565,12.0647323 94.565,12.0357143 L94.565,11.8214286 C94.565,11.7924106 94.5756026,11.7672992 94.596808,11.7460937 C94.6180135,11.7248883 94.6431249,11.7142857 94.6721429,11.7142857 L97.0292857,11.7142857 C97.0583037,11.7142857 97.0834151,11.7248883 97.1046205,11.7460937 C97.125826,11.7672992 97.1364286,11.7924106 97.1364286,11.8214286 L97.1364286,11.8214286 Z M97.1364286,10.9642857 L97.1364286,11.1785714 C97.1364286,11.2075894 97.125826,11.2327008 97.1046205,11.2539062 C97.0834151,11.2751117 97.0583037,11.2857143 97.0292857,11.2857143 L94.6721429,11.2857143 C94.6431249,11.2857143 94.6180135,11.2751117 94.596808,11.2539062 C94.5756026,11.2327008 94.565,11.2075894 94.565,11.1785714 L94.565,10.9642857 C94.565,10.9352677 94.5756026,10.9101564 94.596808,10.8889509 C94.6180135,10.8677454 94.6431249,10.8571429 94.6721429,10.8571429 L97.0292857,10.8571429 C97.0583037,10.8571429 97.0834151,10.8677454 97.1046205,10.8889509 C97.125826,10.9101564 97.1364286,10.9352677 97.1364286,10.9642857 L97.1364286,10.9642857 Z" fill="#FFFFFF"/>
</g>
<rect stroke="#ABE6F8" x="0" y="0" width="101" height="62"/>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 7.7 KiB

Просмотреть файл

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="102px" height="62px" viewBox="0 0 102 62" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(0.500000, 0.000000)">
<rect stroke="#AAE5F9" fill="#FFFFFF" x="0.990196078" y="1" width="99.0196078" height="60"/>
<rect fill="#AAE5F9" x="2.97058824" y="3" width="33.6666667" height="13"/>
<rect fill="#AAE5F9" x="0.990196078" y="8" width="99.0196078" height="10"/>
<path d="M27.2559789,11 C25.8746722,11 24.754902,12.1096621 24.754902,13.5 L24.754902,13.5 C24.754902,14.8807119 25.8688951,16 27.2538514,16 L46.91717,16 L95.5280221,16 C96.9095015,16 98.0294118,14.8903379 98.0294118,13.5 L98.0294118,13.5 C98.0294118,12.1192881 96.9077658,11 95.5283349,11 L27.2559789,11 Z" fill="#FFFFFF"/>
<rect stroke="#92CB3E" stroke-width="2" fill="#FFFFFF" x="28.7156863" y="15" width="42.5784314" height="22" rx="2"/>
<path d="M51.5228862,29 C50.1376565,29 49.0147059,30.1096621 49.0147059,31.5 L49.0147059,31.5 C49.0147059,32.8807119 50.1327688,34 51.5124335,34 L55.0045081,34 L66.3156937,34 C67.6980258,34 68.8186275,32.8903379 68.8186275,31.5 L68.8186275,31.5 C68.8186275,30.1192881 67.6971405,29 66.3104471,29 L51.5228862,29 Z" stroke="#92CB3E" fill="#FFFFFF"/>
<rect stroke="#ABE6F8" x="0" y="0" width="101" height="62"/>
<path d="M40.6764706,19.7142857 L40.6764706,24.5714286 C40.6764706,24.6964292 40.6184355,24.7842259 40.5023634,24.8348214 C40.4636728,24.8497025 40.4264708,24.8571429 40.3907563,24.8571429 C40.3103988,24.8571429 40.2434351,24.8288693 40.1898634,24.7723214 L38.3907563,22.9732143 L38.3907563,23.7142857 C38.3907563,24.0684542 38.2650135,24.3712785 38.0135242,24.6227679 C37.7620348,24.8742572 37.4592105,25 37.105042,25 L33.9621849,25 C33.6080164,25 33.3051921,24.8742572 33.0537027,24.6227679 C32.8022134,24.3712785 32.6764706,24.0684542 32.6764706,23.7142857 L32.6764706,20.5714286 C32.6764706,20.2172601 32.8022134,19.9144358 33.0537027,19.6629464 C33.3051921,19.4114571 33.6080164,19.2857143 33.9621849,19.2857143 L37.105042,19.2857143 C37.4592105,19.2857143 37.7620348,19.4114571 38.0135242,19.6629464 C38.2650135,19.9144358 38.3907563,20.2172601 38.3907563,20.5714286 L38.3907563,21.3080357 L40.1898634,19.5133929 C40.2434351,19.456845 40.3103988,19.4285714 40.3907563,19.4285714 C40.4264708,19.4285714 40.4636728,19.4360118 40.5023634,19.4508929 C40.6184355,19.5014883 40.6764706,19.5892851 40.6764706,19.7142857 L40.6764706,19.7142857 Z" fill="#00AF84"/>
<path d="M22.2422969,14.5714286 L22.2422969,14.8571429 C22.2422969,14.8958335 22.2281602,14.9293153 22.1998862,14.9575893 C22.1716123,14.9858632 22.1381304,15 22.0994398,15 L18.9565826,15 C18.917892,15 18.8844102,14.9858632 18.8561362,14.9575893 C18.8278623,14.9293153 18.8137255,14.8958335 18.8137255,14.8571429 L18.8137255,14.5714286 C18.8137255,14.5327379 18.8278623,14.4992561 18.8561362,14.4709821 C18.8844102,14.4427082 18.917892,14.4285714 18.9565826,14.4285714 L22.0994398,14.4285714 C22.1381304,14.4285714 22.1716123,14.4427082 22.1998862,14.4709821 C22.2281602,14.4992561 22.2422969,14.5327379 22.2422969,14.5714286 L22.2422969,14.5714286 Z M22.2422969,13.4285714 L22.2422969,13.7142857 C22.2422969,13.7529764 22.2281602,13.7864582 22.1998862,13.8147321 C22.1716123,13.8430061 22.1381304,13.8571429 22.0994398,13.8571429 L18.9565826,13.8571429 C18.917892,13.8571429 18.8844102,13.8430061 18.8561362,13.8147321 C18.8278623,13.7864582 18.8137255,13.7529764 18.8137255,13.7142857 L18.8137255,13.4285714 C18.8137255,13.3898808 18.8278623,13.356399 18.8561362,13.328125 C18.8844102,13.299851 18.917892,13.2857143 18.9565826,13.2857143 L22.0994398,13.2857143 C22.1381304,13.2857143 22.1716123,13.299851 22.1998862,13.328125 C22.2281602,13.356399 22.2422969,13.3898808 22.2422969,13.4285714 L22.2422969,13.4285714 Z M22.2422969,12.2857143 L22.2422969,12.5714286 C22.2422969,12.6101192 22.2281602,12.643601 22.1998862,12.671875 C22.1716123,12.700149 22.1381304,12.7142857 22.0994398,12.7142857 L18.9565826,12.7142857 C18.917892,12.7142857 18.8844102,12.700149 18.8561362,12.671875 C18.8278623,12.643601 18.8137255,12.6101192 18.8137255,12.5714286 L18.8137255,12.2857143 C18.8137255,12.2470236 18.8278623,12.2135418 18.8561362,12.1852679 C18.8844102,12.1569939 18.917892,12.1428571 18.9565826,12.1428571 L22.0994398,12.1428571 C22.1381304,12.1428571 22.1716123,12.1569939 22.1998862,12.1852679 C22.2281602,12.2135418 22.2422969,12.2470236 22.2422969,12.2857143 L22.2422969,12.2857143 Z" fill="#FFFFFF"/>
<path d="M7.38935574,13.5714286 L7.38935574,13.8571429 C7.38935574,13.9360123 7.36517444,14.0033479 7.3168111,14.0591518 C7.26844776,14.1149556 7.20557637,14.1428571 7.12819503,14.1428571 L5.55676646,14.1428571 L6.21078431,14.7991071 C6.26733222,14.8526788 6.29560574,14.9196425 6.29560574,15 C6.29560574,15.0803575 6.26733222,15.1473212 6.21078431,15.2008929 L6.0433736,15.3705357 C5.9883138,15.4255955 5.92135018,15.453125 5.84248074,15.453125 C5.7650994,15.453125 5.69739175,15.4255955 5.63935574,15.3705357 L4.18623074,13.9151786 C4.13117094,13.8601188 4.10364146,13.7931552 4.10364146,13.7142857 C4.10364146,13.6369044 4.13117094,13.5691967 4.18623074,13.5111607 L5.63935574,12.0602679 C5.69590364,12.00372 5.7636113,11.9754464 5.84248074,11.9754464 C5.91986208,11.9754464 5.9868257,12.00372 6.0433736,12.0602679 L6.21078431,12.2254464 C6.26733222,12.2819943 6.29560574,12.349702 6.29560574,12.4285714 C6.29560574,12.5074409 6.26733222,12.5751485 6.21078431,12.6316964 L5.55676646,13.2857143 L7.12819503,13.2857143 C7.20557637,13.2857143 7.26844776,13.3136158 7.3168111,13.3694196 C7.36517444,13.4252235 7.38935574,13.4925591 7.38935574,13.5714286 L7.38935574,13.5714286 Z" fill="#FFFFFF"/>
<path d="M12.3403361,13.5714286 L12.3403361,13.8571429 C12.3403361,13.9360123 12.3161548,14.0033479 12.2677915,14.0591518 C12.2194282,14.1149556 12.1565568,14.1428571 12.0791754,14.1428571 L10.5077468,14.1428571 L11.1617647,14.7991071 C11.2183126,14.8526788 11.2465861,14.9196425 11.2465861,15 C11.2465861,15.0803575 11.2183126,15.1473212 11.1617647,15.2008929 L10.994354,15.3705357 C10.9392942,15.4255955 10.8723306,15.453125 10.7934611,15.453125 C10.7160798,15.453125 10.6483721,15.4255955 10.5903361,15.3705357 L9.13721113,13.9151786 C9.08215134,13.8601188 9.05462185,13.7931552 9.05462185,13.7142857 C9.05462185,13.6369044 9.08215134,13.5691967 9.13721113,13.5111607 L10.5903361,12.0602679 C10.646884,12.00372 10.7145917,11.9754464 10.7934611,11.9754464 C10.8708425,11.9754464 10.9378061,12.00372 10.994354,12.0602679 L11.1617647,12.2254464 C11.2183126,12.2819943 11.2465861,12.349702 11.2465861,12.4285714 C11.2465861,12.5074409 11.2183126,12.5751485 11.1617647,12.6316964 L10.5077468,13.2857143 L12.0791754,13.2857143 C12.1565568,13.2857143 12.2194282,13.3136158 12.2677915,13.3694196 C12.3161548,13.4252235 12.3403361,13.4925591 12.3403361,13.5714286 L12.3403361,13.5714286 Z" fill="#FFFFFF" transform="translate(10.697479, 13.714286) scale(-1, 1) translate(-10.697479, -13.714286) "/>
<path d="M17.2913165,12.1428571 L17.2913165,13.1428571 C17.2913165,13.1815478 17.2771798,13.2150296 17.2489058,13.2433036 C17.2206319,13.2715775 17.1871501,13.2857143 17.1484594,13.2857143 L16.1484594,13.2857143 C16.0859591,13.2857143 16.0420607,13.2559527 16.016763,13.1964286 C15.9914652,13.1383926 16.0018818,13.0870538 16.048013,13.0424107 L16.3560487,12.734375 C16.1358095,12.5305049 15.8761395,12.4285714 15.5770308,12.4285714 C15.4222681,12.4285714 15.2745762,12.4587051 15.1339505,12.5189732 C14.9933248,12.5792414 14.8716742,12.6607138 14.7689951,12.7633929 C14.666316,12.8660719 14.5848436,12.9877225 14.5245755,13.1283482 C14.4643073,13.2689739 14.4341737,13.4166659 14.4341737,13.5714286 C14.4341737,13.7261912 14.4643073,13.8738832 14.5245755,14.0145089 C14.5848436,14.1551346 14.666316,14.2767852 14.7689951,14.3794643 C14.8716742,14.4821434 14.9933248,14.5636158 15.1339505,14.6238839 C15.2745762,14.6841521 15.4222681,14.7142857 15.5770308,14.7142857 C15.754115,14.7142857 15.9215241,14.6755956 16.079263,14.5982143 C16.2370018,14.5208329 16.370185,14.411459 16.4788165,14.2700893 C16.4892332,14.2552083 16.5063462,14.2462798 16.5301558,14.2433036 C16.5509892,14.2433036 16.5695903,14.2499999 16.5859594,14.2633929 L16.891763,14.5714286 C16.9051559,14.5833334 16.9122243,14.5985862 16.9129683,14.6171875 C16.9137124,14.6357888 16.9081321,14.6525297 16.8962272,14.6674107 C16.734024,14.8638403 16.5375974,15.0159965 16.3069415,15.1238839 C16.0762856,15.2317714 15.8329845,15.2857143 15.5770308,15.2857143 C15.3448868,15.2857143 15.1231628,15.2403278 14.9118522,15.1495536 C14.7005417,15.0587793 14.5182518,14.9367567 14.3649772,14.7834821 C14.2117027,14.6302076 14.0896801,14.4479177 13.9989058,14.2366071 C13.9081315,14.0252966 13.8627451,13.8035726 13.8627451,13.5714286 C13.8627451,13.3392846 13.9081315,13.1175606 13.9989058,12.90625 C14.0896801,12.6949394 14.2117027,12.5126496 14.3649772,12.359375 C14.5182518,12.2061004 14.7005417,12.0840778 14.9118522,11.9933036 C15.1231628,11.9025293 15.3448868,11.8571429 15.5770308,11.8571429 C15.7957819,11.8571429 16.0074613,11.8984371 16.2120755,11.9810268 C16.4166896,12.0636165 16.5986074,12.1800588 16.7578344,12.3303571 L17.048013,12.0424107 C17.0911679,11.9962795 17.1432507,11.985863 17.204263,12.0111607 C17.262299,12.0364585 17.2913165,12.0803568 17.2913165,12.1428571 L17.2913165,12.1428571 Z" fill="#FFFFFF"/>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 9.3 KiB

Просмотреть файл

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="51px" height="36px" viewBox="0 0 51 36" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(0.500000, 0.000000)">
<path d="M50,2.96428571 L50,33.3214286 C50,34.1026825 49.6372804,34.6514121 48.9118304,34.9676339 C48.6700137,35.0606403 48.4375011,35.1071429 48.2142857,35.1071429 C47.7120511,35.1071429 47.2935285,34.9304333 46.9587054,34.5770089 L35.7142857,23.3325893 L35.7142857,27.9642857 C35.7142857,30.1778384 34.9283933,32.0704907 33.3565848,33.6422991 C31.7847764,35.2141076 29.8921242,36 27.6785714,36 L8.03571429,36 C5.82216155,36 3.92950935,35.2141076 2.35770089,33.6422991 C0.785892439,32.0704907 0,30.1778384 0,27.9642857 L0,8.32142857 C0,6.10787584 0.785892439,4.21522363 2.35770089,2.64341518 C3.92950935,1.07160672 5.82216155,0.285714286 8.03571429,0.285714286 L27.6785714,0.285714286 C29.8921242,0.285714286 31.7847764,1.07160672 33.3565848,2.64341518 C34.9283933,4.21522363 35.7142857,6.10787584 35.7142857,8.32142857 L35.7142857,12.9252232 L46.9587054,1.70870536 C47.2935285,1.35528097 47.7120511,1.17857143 48.2142857,1.17857143 C48.4375011,1.17857143 48.6700137,1.22507394 48.9118304,1.31808036 C49.6372804,1.63430218 50,2.18303181 50,2.96428571 L50,2.96428571 Z" fill="#00AF84"/>
<path d="M19.8214286,22.09375 L19.8214286,25.4419643 C19.8214286,25.5907746 19.7656256,25.7209816 19.6540179,25.8325893 C19.5424102,25.944197 19.4122031,26 19.2633929,26 L15.9151786,26 C15.7663683,26 15.6361613,25.944197 15.5245536,25.8325893 C15.4129459,25.7209816 15.3571429,25.5907746 15.3571429,25.4419643 L15.3571429,22.09375 C15.3571429,21.9449397 15.4129459,21.8147327 15.5245536,21.703125 C15.6361613,21.5915173 15.7663683,21.5357143 15.9151786,21.5357143 L19.2633929,21.5357143 C19.4122031,21.5357143 19.5424102,21.5915173 19.6540179,21.703125 C19.7656256,21.8147327 19.8214286,21.9449397 19.8214286,22.09375 L19.8214286,22.09375 Z M24.2299107,13.7232143 C24.2299107,14.2254489 24.1578318,14.6951243 24.0136719,15.1322545 C23.8695119,15.5693846 23.7067531,15.9251288 23.5253906,16.1994978 C23.3440281,16.4738667 23.0882643,16.7505566 22.7580915,17.0295759 C22.4279187,17.3085951 22.1605293,17.5108811 21.9559152,17.6364397 C21.7513011,17.7619984 21.4676357,17.9270823 21.1049107,18.1316964 C20.7235844,18.3456112 20.4050422,18.6478775 20.1492746,19.0385045 C19.8935069,19.4291314 19.765625,19.7406982 19.765625,19.9732143 C19.765625,20.1313252 19.709822,20.2824584 19.5982143,20.4266183 C19.4866066,20.5707783 19.3563996,20.6428571 19.2075893,20.6428571 L15.859375,20.6428571 C15.7198654,20.6428571 15.601284,20.5568275 15.5036272,20.3847656 C15.4059705,20.2127038 15.3571429,20.0383193 15.3571429,19.8616071 L15.3571429,19.233817 C15.3571429,18.4618637 15.6594092,17.7340994 16.2639509,17.0505022 C16.8684926,16.3669051 17.5334785,15.8623528 18.2589286,15.5368304 C18.8076664,15.285713 19.1982875,15.025299 19.4308036,14.7555804 C19.6633196,14.4858617 19.7795759,14.1324427 19.7795759,13.6953125 C19.7795759,13.3046855 19.5633392,12.960567 19.1308594,12.6629464 C18.6983795,12.3653259 18.1984775,12.2165179 17.6311384,12.2165179 C17.0265967,12.2165179 16.5243696,12.3513751 16.124442,12.6210938 C15.7989195,12.8536098 15.3013426,13.3883887 14.6316964,14.2254464 C14.5107881,14.3742567 14.3666303,14.4486607 14.1992188,14.4486607 C14.087611,14.4486607 13.9713548,14.4114587 13.8504464,14.3370536 L11.5625,12.593192 C11.4415917,12.5001855 11.3695128,12.3839293 11.3462612,12.2444196 C11.3230096,12.10491 11.3485859,11.974703 11.4229911,11.8537946 C12.9110938,9.37982394 15.0688103,8.14285714 17.8962054,8.14285714 C18.6402567,8.14285714 19.3889471,8.28701493 20.1422991,8.57533482 C20.8956511,8.86365472 21.5745878,9.24962556 22.1791295,9.73325893 C22.7836712,10.2168923 23.2765978,10.8097993 23.6579241,11.5119978 C24.0392504,12.2141962 24.2299107,12.951261 24.2299107,13.7232143 L24.2299107,13.7232143 Z" fill="#FFFFFF"/>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 3.9 KiB

Просмотреть файл

@ -83,10 +83,20 @@ loop.standaloneRoomViews = (function(mozL10n) {
case ROOM_STATES.MEDIA_WAIT: { case ROOM_STATES.MEDIA_WAIT: {
var msg = mozL10n.get("call_progress_getting_media_description", var msg = mozL10n.get("call_progress_getting_media_description",
{clientShortname: mozL10n.get("clientShortname2")}); {clientShortname: mozL10n.get("clientShortname2")});
// XXX Bug 1047040 will add images to help prompt the user. var utils = loop.shared.utils;
var isChrome = utils.isChrome(navigator.userAgent);
var isFirefox = utils.isFirefox(navigator.userAgent);
var isOpera = utils.isOpera(navigator.userAgent);
var promptMediaMessageClasses = React.addons.classSet({
"prompt-media-message": true,
"chrome": isChrome,
"firefox": isFirefox,
"opera": isOpera,
"other": !isChrome && !isFirefox && !isOpera
});
return ( return (
React.createElement("div", {className: "room-inner-info-area"}, React.createElement("div", {className: "room-inner-info-area"},
React.createElement("p", {className: "prompt-media-message"}, React.createElement("p", {className: promptMediaMessageClasses},
msg msg
) )
) )

Просмотреть файл

@ -83,10 +83,20 @@ loop.standaloneRoomViews = (function(mozL10n) {
case ROOM_STATES.MEDIA_WAIT: { case ROOM_STATES.MEDIA_WAIT: {
var msg = mozL10n.get("call_progress_getting_media_description", var msg = mozL10n.get("call_progress_getting_media_description",
{clientShortname: mozL10n.get("clientShortname2")}); {clientShortname: mozL10n.get("clientShortname2")});
// XXX Bug 1047040 will add images to help prompt the user. var utils = loop.shared.utils;
var isChrome = utils.isChrome(navigator.userAgent);
var isFirefox = utils.isFirefox(navigator.userAgent);
var isOpera = utils.isOpera(navigator.userAgent);
var promptMediaMessageClasses = React.addons.classSet({
"prompt-media-message": true,
"chrome": isChrome,
"firefox": isFirefox,
"opera": isOpera,
"other": !isChrome && !isFirefox && !isOpera
});
return ( return (
<div className="room-inner-info-area"> <div className="room-inner-info-area">
<p className="prompt-media-message"> <p className={promptMediaMessageClasses}>
{msg} {msg}
</p> </p>
</div> </div>

Просмотреть файл

@ -178,6 +178,8 @@ public class BrowserApp extends GeckoApp
public ViewFlipper mActionBarFlipper; public ViewFlipper mActionBarFlipper;
public ActionModeCompatView mActionBar; public ActionModeCompatView mActionBar;
private BrowserToolbar mBrowserToolbar; private BrowserToolbar mBrowserToolbar;
// We can't name the TabStrip class because it's not included on API 9.
private Refreshable mTabStrip;
private ToolbarProgressView mProgressView; private ToolbarProgressView mProgressView;
private FirstrunPane mFirstrunPane; private FirstrunPane mFirstrunPane;
private HomePager mHomePager; private HomePager mHomePager;
@ -191,6 +193,7 @@ public class BrowserApp extends GeckoApp
private static final int GECKO_TOOLS_MENU = -1; private static final int GECKO_TOOLS_MENU = -1;
private static final int ADDON_MENU_OFFSET = 1000; private static final int ADDON_MENU_OFFSET = 1000;
public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment"; public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";
private static class MenuItemInfo { private static class MenuItemInfo {
public int id; public int id;
public String label; public String label;
@ -721,7 +724,7 @@ public class BrowserApp extends GeckoApp
} }
if (HardwareUtils.isTablet()) { if (HardwareUtils.isTablet()) {
findViewById(R.id.new_tablet_tab_strip).setVisibility(View.VISIBLE); mTabStrip = (Refreshable) (((ViewStub) findViewById(R.id.new_tablet_tab_strip)).inflate());
} }
((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideOnTouchListener()); ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideOnTouchListener());
@ -1528,6 +1531,10 @@ public class BrowserApp extends GeckoApp
mTabsPanel.refresh(); mTabsPanel.refresh();
} }
if (mTabStrip != null) {
mTabStrip.refresh();
}
mBrowserToolbar.refresh(); mBrowserToolbar.refresh();
} }
@ -3509,6 +3516,12 @@ public class BrowserApp extends GeckoApp
return GeckoProfile.getDefaultProfileName(this); return GeckoProfile.getDefaultProfileName(this);
} }
// For use from tests only.
@RobocopTarget
public ReadingListHelper getReadingListHelper() {
return mReadingListHelper;
}
/** /**
* Launch UI that lets the user update Firefox. * Launch UI that lets the user update Firefox.
* *
@ -3603,4 +3616,8 @@ public class BrowserApp extends GeckoApp
appLocale, appLocale,
previousSession); previousSession);
} }
public static interface Refreshable {
public void refresh();
}
} }

Просмотреть файл

@ -11,6 +11,7 @@ import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.DBUtils; import org.mozilla.gecko.db.DBUtils;
import org.mozilla.gecko.db.ReadingListAccessor; import org.mozilla.gecko.db.ReadingListAccessor;
import org.mozilla.gecko.favicons.Favicons; import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.mozglue.RobocopTarget;
import org.mozilla.gecko.util.EventCallback; import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener; import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject; import org.mozilla.gecko.util.NativeJSObject;
@ -34,6 +35,8 @@ public final class ReadingListHelper implements NativeEventListener {
private final ReadingListAccessor readingListAccessor; private final ReadingListAccessor readingListAccessor;
private final ContentObserver contentObserver; private final ContentObserver contentObserver;
volatile boolean fetchInBackground = true;
public ReadingListHelper(Context context, GeckoProfile profile) { public ReadingListHelper(Context context, GeckoProfile profile) {
this.context = context; this.context = context;
this.db = profile.getDB(); this.db = profile.getDB();
@ -46,7 +49,9 @@ public final class ReadingListHelper implements NativeEventListener {
contentObserver = new ContentObserver(null) { contentObserver = new ContentObserver(null) {
@Override @Override
public void onChange(boolean selfChange) { public void onChange(boolean selfChange) {
fetchContent(); if (fetchInBackground) {
fetchContent();
}
} }
}; };
@ -139,21 +144,50 @@ public final class ReadingListHelper implements NativeEventListener {
if (message.has("id")) { if (message.has("id")) {
values.put(ReadingListItems._ID, message.getInt("id")); values.put(ReadingListItems._ID, message.getInt("id"));
} }
// url is actually required...
String url = null;
if (message.has("url")) { if (message.has("url")) {
values.put(ReadingListItems.URL, message.getString("url")); url = message.getString("url");
values.put(ReadingListItems.URL, url);
} }
String title = null;
if (message.has("title")) { if (message.has("title")) {
values.put(ReadingListItems.TITLE, message.getString("title")); title = message.getString("title");
values.put(ReadingListItems.TITLE, title);
} }
if (message.has("length")) {
values.put(ReadingListItems.LENGTH, message.getInt("length")); // TODO: message actually has "length", but that's no use for us. See Bug 1127451.
if (message.has("word_count")) {
values.put(ReadingListItems.WORD_COUNT, message.getInt("word_count"));
} }
if (message.has("excerpt")) { if (message.has("excerpt")) {
values.put(ReadingListItems.EXCERPT, message.getString("excerpt")); values.put(ReadingListItems.EXCERPT, message.getString("excerpt"));
} }
if (message.has("status")) { if (message.has("status")) {
values.put(ReadingListItems.CONTENT_STATUS, message.getInt("status")); final int status = message.getInt("status");
values.put(ReadingListItems.CONTENT_STATUS, status);
if (status == ReadingListItems.STATUS_FETCHED_ARTICLE) {
if (message.has("resolved_title")) {
values.put(ReadingListItems.RESOLVED_TITLE, message.getString("resolved_title"));
} else {
if (title != null) {
values.put(ReadingListItems.RESOLVED_TITLE, title);
}
}
if (message.has("resolved_url")) {
values.put(ReadingListItems.RESOLVED_URL, message.getString("resolved_url"));
} else {
if (url != null) {
values.put(ReadingListItems.RESOLVED_URL, url);
}
}
}
} }
return values; return values;
} }
@ -258,4 +292,13 @@ public final class ReadingListHelper implements NativeEventListener {
} }
}); });
} }
@RobocopTarget
/**
* Test code will want to disable background fetches to avoid upsetting
* the test harness. Call this by accessing the instance from BrowserApp.
*/
public void disableBackgroundFetches() {
fetchInBackground = false;
}
} }

Просмотреть файл

@ -342,19 +342,36 @@ public class BrowserContract {
} }
@RobocopTarget @RobocopTarget
public static final class ReadingListItems implements CommonColumns, URLColumns, SyncColumns { public static final class ReadingListItems implements CommonColumns, URLColumns {
public static final String EXCERPT = "excerpt";
public static final String CLIENT_LAST_MODIFIED = "client_last_modified";
public static final String GUID = "guid";
public static final String SERVER_LAST_MODIFIED = "last_modified";
public static final String SERVER_STORED_ON = "stored_on";
public static final String ADDED_ON = "added_on";
public static final String MARKED_READ_ON = "marked_read_on";
public static final String IS_DELETED = "is_deleted";
public static final String IS_ARCHIVED = "is_archived";
public static final String IS_UNREAD = "is_unread";
public static final String IS_ARTICLE = "is_article";
public static final String IS_FAVORITE = "is_favorite";
public static final String RESOLVED_URL = "resolved_url";
public static final String RESOLVED_TITLE = "resolved_title";
public static final String ADDED_BY = "added_by";
public static final String MARKED_READ_BY = "marked_read_by";
public static final String WORD_COUNT = "word_count";
public static final String READ_POSITION = "read_position";
public static final String CONTENT_STATUS = "content_status";
public static final String SYNC_STATUS = "sync_status";
public static final String SYNC_CHANGE_FLAGS = "sync_change_flags";
private ReadingListItems() {} private ReadingListItems() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(READING_LIST_AUTHORITY_URI, "items"); public static final Uri CONTENT_URI = Uri.withAppendedPath(READING_LIST_AUTHORITY_URI, "items");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/readinglistitem"; public static final String CONTENT_TYPE = "vnd.android.cursor.dir/readinglistitem";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/readinglistitem"; public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/readinglistitem";
public static final String EXCERPT = "excerpt";
public static final String READ = "read";
public static final String LENGTH = "length";
public static final String CONTENT_STATUS = "content_status";
// CONTENT_STATUS represents the result of an attempt to fetch content for the reading list item. // CONTENT_STATUS represents the result of an attempt to fetch content for the reading list item.
public static final int STATUS_UNFETCHED = 0; public static final int STATUS_UNFETCHED = 0;
public static final int STATUS_FETCH_FAILED_TEMPORARY = 1; public static final int STATUS_FETCH_FAILED_TEMPORARY = 1;
@ -362,8 +379,40 @@ public class BrowserContract {
public static final int STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT = 3; public static final int STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT = 3;
public static final int STATUS_FETCHED_ARTICLE = 4; public static final int STATUS_FETCHED_ARTICLE = 4;
public static final String DEFAULT_SORT_ORDER = DATE_MODIFIED + " DESC"; // See https://github.com/mozilla-services/readinglist/wiki/Client-phases for how this is expected to work.
public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, LENGTH }; //
// If an item is SYNCED, it doesn't need to be uploaded.
//
// If its status is NEW, the entire record should be uploaded.
//
// If DELETED, the record should be deleted. A record can only move into this state from SYNCED; NEW records
// are deleted immediately.
//
public static final int SYNC_STATUS_SYNCED = 0;
public static final int SYNC_STATUS_NEW = 1; // Upload everything.
public static final int SYNC_STATUS_DELETED = 2; // Delete the record from the server.
public static final int SYNC_STATUS_MODIFIED = 3; // Consult SYNC_CHANGE_FLAGS.
// SYNC_CHANGE_FLAG represents the sets of fields that need to be uploaded.
// If its status is only UNREAD_CHANGED (and maybe FAVORITE_CHANGED?), then it can easily be uploaded
// in a fire-and-forget manner. This change can never conflict.
//
// If its status is RESOLVED, then one or more of the content-oriented fields has changed, and a full
// upload of those fields should occur. These can result in conflicts.
//
// Note that these are flags; they should be considered together when deciding on a course of action.
//
// These flags are meaningless for records in any state other than SYNCED. They can be safely altered in
// other states (to avoid having to query to pre-fill a ContentValues), but should be ignored.
public static final int SYNC_CHANGE_NONE = 0;
public static final int SYNC_CHANGE_UNREAD_CHANGED = 1 << 0; // => marked_read_{on,by}, is_unread
public static final int SYNC_CHANGE_FAVORITE_CHANGED = 1 << 1; // => is_favorite
public static final int SYNC_CHANGE_RESOLVED = 1 << 2; // => is_article, resolved_{url,title}, excerpt, word_count
public static final String DEFAULT_SORT_ORDER = CLIENT_LAST_MODIFIED + " DESC";
public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, WORD_COUNT };
// Minimum fields required to create a reading list item. // Minimum fields required to create a reading list item.
public static final String[] REQUIRED_FIELDS = { Bookmarks.URL, Bookmarks.TITLE }; public static final String[] REQUIRED_FIELDS = { Bookmarks.URL, Bookmarks.TITLE };

Просмотреть файл

@ -32,9 +32,9 @@ import android.util.Log;
final class BrowserDatabaseHelper extends SQLiteOpenHelper { final class BrowserDatabaseHelper extends SQLiteOpenHelper {
private static final String LOGTAG = "GeckoBrowserDBHelper"; private static final String LOGTAG = "GeckoBrowserDBHelper";
public static final int DATABASE_VERSION = 22;
public static final int DATABASE_VERSION = 23;
public static final String DATABASE_NAME = "browser.db"; public static final String DATABASE_NAME = "browser.db";
final protected Context mContext; final protected Context mContext;
@ -295,8 +295,10 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
R.string.bookmarks_folder_places, 0); R.string.bookmarks_folder_places, 0);
createOrUpdateAllSpecialFolders(db); createOrUpdateAllSpecialFolders(db);
createReadingListTable(db);
createSearchHistoryTable(db); createSearchHistoryTable(db);
createReadingListTable(db, TABLE_READING_LIST);
didCreateCurrentReadingListTable = true; // Mostly correct, in the absence of transactions.
createReadingListIndices(db, TABLE_READING_LIST);
} }
private void createSearchHistoryTable(SQLiteDatabase db) { private void createSearchHistoryTable(SQLiteDatabase db) {
@ -312,28 +314,54 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
SearchHistory.TABLE_NAME + "(" + SearchHistory.DATE_LAST_VISITED + ")"); SearchHistory.TABLE_NAME + "(" + SearchHistory.DATE_LAST_VISITED + ")");
} }
private void createReadingListTable(SQLiteDatabase db) { private boolean didCreateCurrentReadingListTable = false;
private void createReadingListTable(final SQLiteDatabase db, final String tableName) {
debug("Creating " + TABLE_READING_LIST + " table"); debug("Creating " + TABLE_READING_LIST + " table");
db.execSQL("CREATE TABLE " + TABLE_READING_LIST + "(" + db.execSQL("CREATE TABLE " + tableName + "(" +
ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
ReadingListItems.URL + " TEXT NOT NULL, " + ReadingListItems.GUID + " TEXT UNIQUE, " + // Server-assigned.
ReadingListItems.TITLE + " TEXT, " +
ReadingListItems.EXCERPT + " TEXT, " +
ReadingListItems.READ + " TINYINT DEFAULT 0, " +
ReadingListItems.IS_DELETED + " TINYINT DEFAULT 0, " +
ReadingListItems.GUID + " TEXT UNIQUE NOT NULL, " +
ReadingListItems.DATE_MODIFIED + " INTEGER NOT NULL, " +
ReadingListItems.DATE_CREATED + " INTEGER NOT NULL, " +
ReadingListItems.LENGTH + " INTEGER DEFAULT 0, " +
ReadingListItems.CONTENT_STATUS + " TINYINT DEFAULT " + ReadingListItems.STATUS_UNFETCHED + "); ");
db.execSQL("CREATE INDEX reading_list_url ON " + TABLE_READING_LIST + "(" ReadingListItems.CONTENT_STATUS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.STATUS_UNFETCHED + ", " +
+ ReadingListItems.URL + ")"); ReadingListItems.SYNC_STATUS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.SYNC_STATUS_NEW + ", " +
db.execSQL("CREATE UNIQUE INDEX reading_list_guid ON " + TABLE_READING_LIST + "(" ReadingListItems.SYNC_CHANGE_FLAGS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.SYNC_CHANGE_NONE + ", " +
+ ReadingListItems.GUID + ")");
db.execSQL("CREATE INDEX reading_list_content_status ON " + TABLE_READING_LIST + "(" ReadingListItems.CLIENT_LAST_MODIFIED + " INTEGER NOT NULL, " + // Client time.
+ ReadingListItems.CONTENT_STATUS + ")"); ReadingListItems.SERVER_LAST_MODIFIED + " INTEGER, " + // Server-assigned.
// Server-assigned.
ReadingListItems.SERVER_STORED_ON + " INTEGER, " +
ReadingListItems.ADDED_ON + " INTEGER, " + // Client time. Shouldn't be null, but not enforced. Formerly DATE_CREATED.
ReadingListItems.MARKED_READ_ON + " INTEGER, " +
// These boolean flags represent the server 'status', 'unread', 'is_article', and 'favorite' fields.
ReadingListItems.IS_DELETED + " TINYINT NOT NULL DEFAULT 0, " +
ReadingListItems.IS_ARCHIVED + " TINYINT NOT NULL DEFAULT 0, " +
ReadingListItems.IS_UNREAD + " TINYINT NOT NULL DEFAULT 1, " +
ReadingListItems.IS_ARTICLE + " TINYINT NOT NULL DEFAULT 0, " +
ReadingListItems.IS_FAVORITE + " TINYINT NOT NULL DEFAULT 0, " +
ReadingListItems.URL + " TEXT NOT NULL, " +
ReadingListItems.TITLE + " TEXT, " +
ReadingListItems.RESOLVED_URL + " TEXT, " +
ReadingListItems.RESOLVED_TITLE + " TEXT, " +
ReadingListItems.EXCERPT + " TEXT, " +
ReadingListItems.ADDED_BY + " TEXT, " +
ReadingListItems.MARKED_READ_BY + " TEXT, " +
ReadingListItems.WORD_COUNT + " INTEGER DEFAULT 0, " +
ReadingListItems.READ_POSITION + " INTEGER DEFAULT 0 " +
"); ");
}
private void createReadingListIndices(final SQLiteDatabase db, final String tableName) {
// No need to create an index on GUID; it's a UNIQUE column.
db.execSQL("CREATE INDEX reading_list_url ON " + tableName + "("
+ ReadingListItems.URL + ")");
db.execSQL("CREATE INDEX reading_list_content_status ON " + tableName + "("
+ ReadingListItems.CONTENT_STATUS + ")");
} }
private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) { private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) {
@ -687,8 +715,7 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
} }
/* /*
* Moves reading list items from 'bookmarks' table to 'reading_list' table. Uses the * Moves reading list items from 'bookmarks' table to 'reading_list' table.
* same item GUID.
*/ */
private void upgradeDatabaseFrom17to18(SQLiteDatabase db) { private void upgradeDatabaseFrom17to18(SQLiteDatabase db) {
debug("Moving reading list items from 'bookmarks' table to 'reading_list' table"); debug("Moving reading list items from 'bookmarks' table to 'reading_list' table");
@ -701,49 +728,60 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED,
Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED,
Bookmarks.TITLE }; Bookmarks.TITLE };
Cursor cursor = null;
try { try {
// Start transaction
db.beginTransaction(); db.beginTransaction();
// Create 'reading_list' table // Create 'reading_list' table.
createReadingListTable(db); createReadingListTable(db, TABLE_READING_LIST);
// Get all the reading list items from bookmarks table // Get all the reading list items from bookmarks table.
cursor = db.query(TABLE_BOOKMARKS, projection, selection, selectionArgs, final Cursor cursor = db.query(TABLE_BOOKMARKS, projection, selection, selectionArgs, null, null, null);
null, null, null);
// Insert reading list items into reading_list table if (cursor == null) {
while (cursor.moveToNext()) { // This should never happen.
debug(DatabaseUtils.dumpCurrentRowToString(cursor)); db.setTransactionSuccessful();
ContentValues values = new ContentValues(); return;
DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.URL, values, ReadingListItems.URL);
DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.GUID, values, ReadingListItems.GUID);
DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.TITLE, values, ReadingListItems.TITLE);
DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_CREATED, values, ReadingListItems.DATE_CREATED);
DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_MODIFIED, values, ReadingListItems.DATE_MODIFIED);
db.insertOrThrow(TABLE_READING_LIST, null, values);
} }
// Delete reading list items from bookmarks table try {
// Insert reading list items into reading_list table.
while (cursor.moveToNext()) {
debug(DatabaseUtils.dumpCurrentRowToString(cursor));
final ContentValues values = new ContentValues();
// We don't preserve bookmark GUIDs.
DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.URL, values, ReadingListItems.URL);
DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.TITLE, values, ReadingListItems.TITLE);
DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_CREATED, values, ReadingListItems.ADDED_ON);
DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_MODIFIED, values, ReadingListItems.CLIENT_LAST_MODIFIED);
db.insertOrThrow(TABLE_READING_LIST, null, values);
}
} finally {
cursor.close();
}
// Delete reading list items from bookmarks table.
db.delete(TABLE_BOOKMARKS, db.delete(TABLE_BOOKMARKS,
Bookmarks.PARENT + " = ? ", Bookmarks.PARENT + " = ? ",
new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) });
// Delete reading list special folder // Delete reading list special folder.
db.delete(TABLE_BOOKMARKS, db.delete(TABLE_BOOKMARKS,
Bookmarks._ID + " = ? ", Bookmarks._ID + " = ? ",
new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) });
// Done
// Create indices.
createReadingListIndices(db, TABLE_READING_LIST);
// Done.
db.setTransactionSuccessful(); db.setTransactionSuccessful();
didCreateCurrentReadingListTable = true;
} catch (SQLException e) { } catch (SQLException e) {
Log.e(LOGTAG, "Error migrating reading list items", e); Log.e(LOGTAG, "Error migrating reading list items", e);
} finally { } finally {
if (cursor != null) {
cursor.close();
}
db.endTransaction(); db.endTransaction();
} }
} }
@ -766,6 +804,11 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
} }
private void upgradeDatabaseFrom21to22(SQLiteDatabase db) { private void upgradeDatabaseFrom21to22(SQLiteDatabase db) {
if (didCreateCurrentReadingListTable) {
debug("No need to add CONTENT_STATUS to reading list; we just created with the current schema.");
return;
}
debug("Adding CONTENT_STATUS column to reading list table."); debug("Adding CONTENT_STATUS column to reading list table.");
try { try {
@ -782,6 +825,60 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
} }
} }
private void upgradeDatabaseFrom22to23(SQLiteDatabase db) {
if (didCreateCurrentReadingListTable) {
debug("No need to rev reading list schema; we just created with the current schema.");
return;
}
debug("Rewriting reading list table.");
createReadingListTable(db, "tmp_rl");
// Remove indexes. We don't need them now, and we'll be throwing away the table.
db.execSQL("DROP INDEX IF EXISTS reading_list_url");
db.execSQL("DROP INDEX IF EXISTS reading_list_guid");
db.execSQL("DROP INDEX IF EXISTS reading_list_content_status");
final String thisDevice = ReadingListProvider.PLACEHOLDER_THIS_DEVICE;
db.execSQL("INSERT INTO tmp_rl (" +
// Here are the columns we can preserve.
ReadingListItems._ID + ", " +
ReadingListItems.URL + ", " +
ReadingListItems.TITLE + ", " +
ReadingListItems.RESOLVED_TITLE + ", " + // = TITLE (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE)
ReadingListItems.RESOLVED_URL + ", " + // = URL (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE)
ReadingListItems.EXCERPT + ", " +
ReadingListItems.IS_UNREAD + ", " + // = !READ
ReadingListItems.IS_DELETED + ", " + // = 0
ReadingListItems.GUID + ", " + // = NULL
ReadingListItems.CLIENT_LAST_MODIFIED + ", " + // = DATE_MODIFIED
ReadingListItems.ADDED_ON + ", " + // = DATE_CREATED
ReadingListItems.CONTENT_STATUS + ", " +
ReadingListItems.MARKED_READ_BY + ", " + // if READ + ", = this device
ReadingListItems.ADDED_BY + // = this device
") " +
"SELECT " +
"_id, url, title, " +
"CASE content_status WHEN " + ReadingListItems.STATUS_FETCHED_ARTICLE + " THEN title ELSE NULL END, " + // RESOLVED_TITLE.
"CASE content_status WHEN " + ReadingListItems.STATUS_FETCHED_ARTICLE + " THEN url ELSE NULL END, " + // RESOLVED_URL.
"excerpt, " +
"CASE read WHEN 1 THEN 0 ELSE 1 END, " + // IS_UNREAD.
"0, " + // IS_DELETED.
"NULL, modified, created, content_status, " +
"CASE read WHEN 1 THEN ? ELSE NULL END, " + // MARKED_READ_BY.
"?" + // ADDED_BY.
" FROM " + TABLE_READING_LIST +
" WHERE deleted = 0",
new String[] {thisDevice, thisDevice});
// Now switch these tables over and recreate the indices.
db.execSQL("DROP TABLE " + TABLE_READING_LIST);
db.execSQL("ALTER TABLE tmp_rl RENAME TO " + TABLE_READING_LIST);
createReadingListIndices(db, TABLE_READING_LIST);
}
private void createV19CombinedView(SQLiteDatabase db) { private void createV19CombinedView(SQLiteDatabase db) {
db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS); db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
@ -847,11 +944,11 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
break; break;
case 22: case 22:
if (oldVersion <= 17) { upgradeDatabaseFrom21to22(db);
// We just created the right table in 17to18. Do nothing here. break;
} else {
upgradeDatabaseFrom21to22(db); case 23:
} upgradeDatabaseFrom22to23(db);
break; break;
} }
} }

Просмотреть файл

@ -5,62 +5,75 @@
package org.mozilla.gecko.db; package org.mozilla.gecko.db;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.mozglue.RobocopTarget;
@RobocopTarget
public class LocalReadingListAccessor implements ReadingListAccessor { public class LocalReadingListAccessor implements ReadingListAccessor {
private static final String LOG_TAG = "GeckoReadingListAcc"; private static final String LOG_TAG = "GeckoReadingListAcc";
private static final String NOT_DELETED = ReadingListItems.IS_DELETED + " = 0";
private static final String NEITHER_DELETED_NOR_ARCHIVED = ReadingListItems.IS_ARCHIVED + " = 0 AND " + ReadingListItems.IS_DELETED + " = 0";
private static final String ITEMS_TO_FETCH = ReadingListItems.CONTENT_STATUS + " = " + ReadingListItems.STATUS_UNFETCHED + " AND " + NEITHER_DELETED_NOR_ARCHIVED;
private static final String SORT_ORDER_RECENT_FIRST = "COALESCE(" + ReadingListItems.SERVER_STORED_ON + ", " + ReadingListItems.ADDED_ON + ") DESC";
private final Uri mReadingListUriWithProfile; private final Uri mReadingListUriWithProfile;
public LocalReadingListAccessor(final String profile) { public LocalReadingListAccessor(final String profile) {
mReadingListUriWithProfile = DBUtils.appendProfile(profile, BrowserContract.ReadingListItems.CONTENT_URI); mReadingListUriWithProfile = DBUtils.appendProfile(profile, ReadingListItems.CONTENT_URI);
} }
// Return a count of non-deleted items.
@Override @Override
public int getCount(ContentResolver cr) { public int getCount(ContentResolver cr) {
final String[] columns = new String[]{BrowserContract.ReadingListItems._ID}; final String[] columns = new String[]{ReadingListItems._ID};
final Cursor cursor = cr.query(mReadingListUriWithProfile, columns, null, null, null); final Cursor cursor = cr.query(mReadingListUriWithProfile, columns, NOT_DELETED, null, null);
int count = 0;
try { try {
count = cursor.getCount(); return cursor.getCount();
} finally { } finally {
cursor.close(); cursor.close();
} }
Log.d(LOG_TAG, "Got count " + count + " for reading list.");
return count;
} }
@Override @Override
public Cursor getReadingList(ContentResolver cr) { public Cursor getReadingList(ContentResolver cr) {
// Return non-deleted, non-archived items, ordered by either server stored data or local added date,
// descending.
// This isn't ideal -- it depends on upload order! -- but the alternative is that a client with a
// very skewed clock will force its items to the front or end of the list on other devices.
return cr.query(mReadingListUriWithProfile, return cr.query(mReadingListUriWithProfile,
BrowserContract.ReadingListItems.DEFAULT_PROJECTION, ReadingListItems.DEFAULT_PROJECTION,
NEITHER_DELETED_NOR_ARCHIVED,
null, null,
null, SORT_ORDER_RECENT_FIRST);
null);
} }
@Override @Override
public Cursor getReadingListUnfetched(ContentResolver cr) { public Cursor getReadingListUnfetched(ContentResolver cr) {
// Return unfetched, non-deleted, non-archived items, sorted by date added, newest first.
// This allows us to fetch the top of the list first.
return cr.query(mReadingListUriWithProfile, return cr.query(mReadingListUriWithProfile,
new String[] { BrowserContract.ReadingListItems._ID, BrowserContract.ReadingListItems.URL }, new String[] { ReadingListItems._ID, ReadingListItems.URL },
BrowserContract.ReadingListItems.CONTENT_STATUS + " = " + BrowserContract.ReadingListItems.STATUS_UNFETCHED, ITEMS_TO_FETCH,
null, null,
null); SORT_ORDER_RECENT_FIRST);
} }
@Override @Override
public boolean isReadingListItem(ContentResolver cr, String uri) { public boolean isReadingListItem(ContentResolver cr, String uri) {
final Cursor c = cr.query(mReadingListUriWithProfile, final Cursor c = cr.query(mReadingListUriWithProfile,
new String[] { BrowserContract.ReadingListItems._ID }, new String[] { ReadingListItems._ID },
BrowserContract.ReadingListItems.URL + " = ? ", ReadingListItems.URL + " = ? OR " + ReadingListItems.RESOLVED_URL + " = ?",
new String[] { uri }, new String[] { uri, uri },
null); null);
if (c == null) { if (c == null) {
@ -69,7 +82,7 @@ public class LocalReadingListAccessor implements ReadingListAccessor {
} }
try { try {
return c.getCount() > 0; return c.moveToNext();
} finally { } finally {
c.close(); c.close();
} }
@ -77,52 +90,91 @@ public class LocalReadingListAccessor implements ReadingListAccessor {
@Override @Override
public void addReadingListItem(ContentResolver cr, ContentValues values) { public long addReadingListItem(ContentResolver cr, ContentValues values) {
// Check that required fields are present. // Check that required fields are present.
for (String field: BrowserContract.ReadingListItems.REQUIRED_FIELDS) { for (String field: ReadingListItems.REQUIRED_FIELDS) {
if (!values.containsKey(field)) { if (!values.containsKey(field)) {
throw new IllegalArgumentException("Missing required field for reading list item: " + field); throw new IllegalArgumentException("Missing required field for reading list item: " + field);
} }
} }
// Clear delete flag if necessary // We're adding locally, so we can specify these.
values.put(BrowserContract.ReadingListItems.IS_DELETED, 0); values.put(ReadingListItems.ADDED_ON, System.currentTimeMillis());
values.put(ReadingListItems.ADDED_BY, ReadingListProvider.PLACEHOLDER_THIS_DEVICE);
// Restore deleted record if possible // We never un-delete (and we can't; we wipe as we go).
final Uri insertUri = mReadingListUriWithProfile // Re-add if necessary and allow the server to resolve conflicts.
.buildUpon() return ContentUris.parseId(cr.insert(mReadingListUriWithProfile, values));
.appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true") }
.build();
final int updated = cr.update(insertUri, @Override
values, public long addBasicReadingListItem(ContentResolver cr, String url, String title) {
BrowserContract.ReadingListItems.URL + " = ? ", if (url == null) {
new String[] { values.getAsString(BrowserContract.ReadingListItems.URL) }); throw new IllegalArgumentException("URL must not be null.");
}
final ContentValues values = new ContentValues();
values.put(ReadingListItems.URL, url);
if (title != null) {
values.put(ReadingListItems.TITLE, title);
} else {
values.putNull(ReadingListItems.TITLE);
}
Log.d(LOG_TAG, "Updated " + updated + " rows to new modified time."); return addReadingListItem(cr, values);
} }
@Override @Override
public void updateReadingListItem(ContentResolver cr, ContentValues values) { public void updateReadingListItem(ContentResolver cr, ContentValues values) {
if (!values.containsKey(BrowserContract.ReadingListItems._ID)) { if (!values.containsKey(ReadingListItems._ID)) {
throw new IllegalArgumentException("Cannot update reading list item without an ID"); throw new IllegalArgumentException("Cannot update reading list item without an ID");
} }
final int updated = cr.update(mReadingListUriWithProfile, final int updated = cr.update(mReadingListUriWithProfile,
values, values,
BrowserContract.ReadingListItems._ID + " = ? ", ReadingListItems._ID + " = ? ",
new String[] { values.getAsString(BrowserContract.ReadingListItems._ID) }); new String[] { values.getAsString(ReadingListItems._ID) });
Log.d(LOG_TAG, "Updated " + updated + " reading list rows."); Log.d(LOG_TAG, "Updated " + updated + " reading list rows.");
} }
@Override @Override
public void removeReadingListItemWithURL(ContentResolver cr, String uri) { public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
cr.delete(mReadingListUriWithProfile, BrowserContract.ReadingListItems.URL + " = ? ", new String[]{uri}); cr.delete(mReadingListUriWithProfile,
ReadingListItems.URL + " = ? OR " + ReadingListItems.RESOLVED_URL + " = ?",
new String[]{ uri, uri });
}
@Override
public void deleteItem(ContentResolver cr, long itemID) {
cr.delete(ContentUris.appendId(mReadingListUriWithProfile.buildUpon(), itemID).build(),
null, null);
} }
@Override @Override
public void registerContentObserver(Context context, ContentObserver observer) { public void registerContentObserver(Context context, ContentObserver observer) {
context.getContentResolver().registerContentObserver(mReadingListUriWithProfile, false, observer); context.getContentResolver().registerContentObserver(mReadingListUriWithProfile, false, observer);
} }
@Override
public void markAsRead(ContentResolver cr, long itemID) {
final ContentValues values = new ContentValues();
values.put(ReadingListItems.MARKED_READ_BY, ReadingListProvider.PLACEHOLDER_THIS_DEVICE);
values.put(ReadingListItems.MARKED_READ_ON, System.currentTimeMillis());
values.put(ReadingListItems.IS_UNREAD, 0);
// The ContentProvider will take care of updating the sync metadata.
cr.update(mReadingListUriWithProfile, values, ReadingListItems._ID + " = " + itemID, null);
}
@Override
public void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt) {
final ContentValues values = new ContentValues();
values.put(ReadingListItems.CONTENT_STATUS, ReadingListItems.STATUS_FETCHED_ARTICLE);
values.put(ReadingListItems.RESOLVED_URL, resolvedURL);
values.put(ReadingListItems.RESOLVED_TITLE, resolvedTitle);
values.put(ReadingListItems.EXCERPT, excerpt);
// The ContentProvider will take care of updating the sync metadata.
cr.update(mReadingListUriWithProfile, values, ReadingListItems._ID + " = " + itemID, null);
}
} }

Просмотреть файл

@ -9,9 +9,14 @@ import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.database.Cursor; import android.database.Cursor;
import org.mozilla.gecko.mozglue.RobocopTarget;
@RobocopTarget
public interface ReadingListAccessor { public interface ReadingListAccessor {
/** /**
* Returns non-deleted, non-archived items.
* Fennec doesn't currently offer a way to display archived items.
*
* Can return <code>null</code>. * Can return <code>null</code>.
*/ */
Cursor getReadingList(ContentResolver cr); Cursor getReadingList(ContentResolver cr);
@ -22,11 +27,16 @@ public interface ReadingListAccessor {
boolean isReadingListItem(ContentResolver cr, String uri); boolean isReadingListItem(ContentResolver cr, String uri);
void addReadingListItem(ContentResolver cr, ContentValues values); long addReadingListItem(ContentResolver cr, ContentValues values);
long addBasicReadingListItem(ContentResolver cr, String url, String title);
void updateReadingListItem(ContentResolver cr, ContentValues values); void updateReadingListItem(ContentResolver cr, ContentValues values);
void removeReadingListItemWithURL(ContentResolver cr, String uri); void removeReadingListItemWithURL(ContentResolver cr, String uri);
void registerContentObserver(Context context, ContentObserver observer); void registerContentObserver(Context context, ContentObserver observer);
void markAsRead(ContentResolver cr, long itemID);
void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt);
void deleteItem(ContentResolver cr, long itemID);
} }

Просмотреть файл

@ -4,9 +4,6 @@
package org.mozilla.gecko.db; package org.mozilla.gecko.db;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.sync.Utils;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.UriMatcher; import android.content.UriMatcher;
@ -16,13 +13,17 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.*;
public class ReadingListProvider extends SharedBrowserDatabaseProvider { public class ReadingListProvider extends SharedBrowserDatabaseProvider {
static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME; static final String TABLE_READING_LIST = TABLE_NAME;
static final int ITEMS = 101; static final int ITEMS = 101;
static final int ITEMS_ID = 102; static final int ITEMS_ID = 102;
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
public static final String PLACEHOLDER_THIS_DEVICE = "$local";
static { static {
URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items", ITEMS); URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items", ITEMS);
URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items/#", ITEMS_ID); URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items/#", ITEMS_ID);
@ -32,81 +33,213 @@ public class ReadingListProvider extends SharedBrowserDatabaseProvider {
* Updates items that match the selection criteria. If no such items is found * Updates items that match the selection criteria. If no such items is found
* one is inserted with the attributes passed in. Returns 0 if no item updated. * one is inserted with the attributes passed in. Returns 0 if no item updated.
* *
* Only use this method for callers, not internally -- it futzes with the provided
* values to set syncing flags.
*
* @return Number of items updated or inserted * @return Number of items updated or inserted
*/ */
public int updateOrInsertItem(Uri uri, ContentValues values, String selection, String[] selectionArgs) { public int updateOrInsertItem(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int updated = updateItems(uri, values, selection, selectionArgs); if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
}
if (isCallerSync(uri)) {
int updated = updateItemsWithFlags(uri, values, null, selection, selectionArgs);
if (updated > 0) {
return updated;
}
return insertItem(uri, values) != -1 ? 1 : 0;
}
// Assume updated.
final ContentValues flags = processChangeValues(values);
int updated = updateItemsWithFlags(uri, values, flags, selection, selectionArgs);
if (updated <= 0) { if (updated <= 0) {
// Must be an insertion. Let's make sure we're NEW and discard any update flags.
values.put(SYNC_STATUS, SYNC_STATUS_NEW);
values.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
updated = insertItem(uri, values) != -1 ? 1 : 0; updated = insertItem(uri, values) != -1 ? 1 : 0;
} }
return updated; return updated;
} }
/**
* This method does two things:
* * Based on the values provided, it computes and returns an incremental status change
* that can be applied to the database to track changes for syncing. This should be
* applied with {@link org.mozilla.gecko.db.DBUtils.UpdateOperation#BITWISE_OR}.
* * It mutates the provided values to mark absolute field changes.
*
* @return null if no values were provided, or no change needs to be recorded.
*/
private ContentValues processChangeValues(ContentValues values) {
if (values == null || values.size() == 0) {
return null;
}
// Otherwise, it must have been modified.
values.put(SYNC_STATUS, SYNC_STATUS_MODIFIED);
final ContentValues out = new ContentValues();
int flag = 0;
if (values.containsKey(MARKED_READ_BY) ||
values.containsKey(MARKED_READ_ON) ||
values.containsKey(IS_UNREAD)) {
flag |= SYNC_CHANGE_UNREAD_CHANGED;
}
if (values.containsKey(IS_FAVORITE)) {
flag |= SYNC_CHANGE_FAVORITE_CHANGED;
}
if (values.containsKey(RESOLVED_URL) ||
values.containsKey(RESOLVED_TITLE) ||
values.containsKey(EXCERPT)) {
flag |= SYNC_CHANGE_RESOLVED;
}
if (flag == 0) {
return null;
}
out.put(SYNC_CHANGE_FLAGS, flag);
return out;
}
/** /**
* Updates items that match the selection criteria. * Updates items that match the selection criteria.
* *
* @return Number of items updated or inserted * @return Number of items updated or inserted
*/ */
public int updateItems(Uri uri, ContentValues values, String selection, String[] selectionArgs) { public int updateItemsWithFlags(Uri uri, ContentValues values, ContentValues flags, String selection, String[] selectionArgs) {
trace("Updating ReadingListItems on URI: " + uri); trace("Updating ReadingListItems on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri); final SQLiteDatabase db = getWritableDatabase(uri);
if (!values.containsKey(ReadingListItems.DATE_MODIFIED)) { if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
values.put(ReadingListItems.DATE_MODIFIED, System.currentTimeMillis()); values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
} }
return db.update(TABLE_READING_LIST, values, selection, selectionArgs);
if (flags == null) {
// Dunno what we're doing with the DB that isn't changing anything we care about, but hey.
return db.update(TABLE_READING_LIST, values, selection, selectionArgs);
}
// Otherwise, we need to do smart updating to change flags.
final ContentValues[] valuesAndFlags = {values, flags};
final DBUtils.UpdateOperation[] ops = {DBUtils.UpdateOperation.ASSIGN, DBUtils.UpdateOperation.BITWISE_OR};
return DBUtils.updateArrays(db, TABLE_READING_LIST, valuesAndFlags, ops, selection, selectionArgs);
} }
/** /**
* Inserts a new item into the DB. DATE_CREATED, DATE_MODIFIED * Inserts a new item into the DB. CLIENT_LAST_MODIFIED is generated if it is not specified.
* and GUID fields are generated if they are not specified. *
* Non-Sync callers will have ADDED_ON and ADDED_BY set appropriately if they are missing;
* the assumption is that this is a new item added on this device.
* *
* @return ID of the newly inserted item * @return ID of the newly inserted item
*/ */
long insertItem(Uri uri, ContentValues values) { private long insertItem(Uri uri, ContentValues values) {
long now = System.currentTimeMillis(); if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
if (!values.containsKey(ReadingListItems.DATE_CREATED)) { values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
values.put(ReadingListItems.DATE_CREATED, now);
} }
if (!values.containsKey(ReadingListItems.DATE_MODIFIED)) { // We trust the syncing code to specify SYNC_STATUS_SYNCED.
values.put(ReadingListItems.DATE_MODIFIED, now); if (!isCallerSync(uri)) {
values.put(SYNC_STATUS, SYNC_STATUS_NEW);
if (!values.containsKey(ADDED_ON)) {
values.put(ADDED_ON, System.currentTimeMillis());
}
if (!values.containsKey(ADDED_BY)) {
values.put(ADDED_BY, PLACEHOLDER_THIS_DEVICE);
}
} }
if (!values.containsKey(ReadingListItems.GUID)) { final String url = values.getAsString(URL);
values.put(ReadingListItems.GUID, Utils.generateGuid());
}
String url = values.getAsString(ReadingListItems.URL);
debug("Inserting item in database with URL: " + url); debug("Inserting item in database with URL: " + url);
return getWritableDatabase(uri) return getWritableDatabase(uri).insertOrThrow(TABLE_READING_LIST, null, values);
.insertOrThrow(TABLE_READING_LIST, null, values); }
private static final ContentValues DELETED_VALUES;
static {
final ContentValues values = new ContentValues();
values.put(IS_DELETED, 1);
values.put(URL, ""); // Non-null column.
values.putNull(RESOLVED_URL);
values.putNull(RESOLVED_TITLE);
values.putNull(TITLE);
values.putNull(EXCERPT);
values.putNull(ADDED_BY);
values.putNull(MARKED_READ_BY);
// Mark it as deleted for sync purposes.
values.put(SYNC_STATUS, SYNC_STATUS_DELETED);
values.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
DELETED_VALUES = values;
} }
/** /**
* Deletes items. Item is marked as 'deleted' so that sync can * Deletes items. Item is marked as 'deleted' so that sync can
* detect the change. * detect the change.
* *
* It's the caller's responsibility to handle both original and resolved URLs.
* @return Number of deleted items * @return Number of deleted items
*/ */
int deleteItems(Uri uri, String selection, String[] selectionArgs) { int deleteItems(final Uri uri, String selection, String[] selectionArgs) {
debug("Deleting item entry for URI: " + uri); debug("Deleting item entry for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri); final SQLiteDatabase db = getWritableDatabase(uri);
// TODO: also ensure that we delete affected items from the disk cache. Bug 1133158.
if (isCallerSync(uri)) { if (isCallerSync(uri)) {
debug("Directly deleting from reading list.");
return db.delete(TABLE_READING_LIST, selection, selectionArgs); return db.delete(TABLE_READING_LIST, selection, selectionArgs);
} }
debug("Marking item entry as deleted for URI: " + uri); // If we don't have a GUID for this item, then it hasn't made it
ContentValues values = new ContentValues(); // to the server. Just delete it.
values.put(ReadingListItems.IS_DELETED, 1); // If we do have a GUID, blank the row and mark it as deleted.
int total = 0;
final String whereNullGUID = DBUtils.concatenateWhere(selection, GUID + " IS NULL");
final String whereNotNullGUID = DBUtils.concatenateWhere(selection, GUID + " IS NOT NULL");
cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST); total += db.delete(TABLE_READING_LIST, whereNullGUID, selectionArgs);
return updateItems(uri, values, selection, selectionArgs); total += updateItemsWithFlags(uri, DELETED_VALUES, null, whereNotNullGUID, selectionArgs);
return total;
}
int deleteItemByID(final Uri uri, long id) {
debug("Deleting item entry for ID: " + id);
final SQLiteDatabase db = getWritableDatabase(uri);
// TODO: also ensure that we delete affected items from the disk cache. Bug 1133158.
if (isCallerSync(uri)) {
debug("Directly deleting from reading list.");
final String selection = _ID + " = " + id;
return db.delete(TABLE_READING_LIST, selection, null);
}
// If we don't have a GUID for this item, then it hasn't made it
// to the server. Just delete it.
final String whereNullGUID = _ID + " = " + id + " AND " + GUID + " IS NULL";
final int raw = db.delete(TABLE_READING_LIST, whereNullGUID, null);
if (raw > 0) {
// _ID is unique, so this should only ever be 1, but it definitely means
// we don't need to try the second part.
return raw;
}
// If we do have a GUID, blank the row and mark it as deleted.
final String whereNotNullGUID = _ID + " = " + id + " AND " + GUID + " IS NOT NULL";
final ContentValues values = new ContentValues(DELETED_VALUES);
values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
return updateItemsWithFlags(uri, values, null, whereNotNullGUID, null);
} }
@Override @Override
@SuppressWarnings("fallthrough") @SuppressWarnings("fallthrough")
public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) { public int updateInTransaction(final Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Calling update in transaction on URI: " + uri); trace("Calling update in transaction on URI: " + uri);
int updated = 0; int updated = 0;
@ -121,9 +254,14 @@ public class ReadingListProvider extends SharedBrowserDatabaseProvider {
case ITEMS: { case ITEMS: {
debug("Updating ITEMS: " + uri); debug("Updating ITEMS: " + uri);
updated = shouldUpdateOrInsert(uri) ? if (shouldUpdateOrInsert(uri)) {
updateOrInsertItem(uri, values, selection, selectionArgs) : // updateOrInsertItem handles change flags for us.
updateItems(uri, values, selection, selectionArgs); updated = updateOrInsertItem(uri, values, selection, selectionArgs);
} else {
// Don't use flags if we're inserting from sync.
ContentValues flags = isCallerSync(uri) ? null : processChangeValues(values);
updated = updateItemsWithFlags(uri, values, flags, selection, selectionArgs);
}
break; break;
} }
@ -141,15 +279,18 @@ public class ReadingListProvider extends SharedBrowserDatabaseProvider {
public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete in transaction on URI: " + uri); trace("Calling delete in transaction on URI: " + uri);
// This will never clean up any items that we're about to delete, so we
// might as well run it first!
cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST);
int numDeleted = 0; int numDeleted = 0;
int match = URI_MATCHER.match(uri); int match = URI_MATCHER.match(uri);
switch (match) { switch (match) {
case ITEMS_ID: case ITEMS_ID:
debug("Deleting on ITEMS_ID: " + uri); debug("Deleting on ITEMS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, TABLE_READING_LIST + "._id = ?"); numDeleted = deleteItemByID(uri, ContentUris.parseId(uri));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, break;
new String[] { Long.toString(ContentUris.parseId(uri)) });
case ITEMS: case ITEMS:
debug("Deleting ITEMS: " + uri); debug("Deleting ITEMS: " + uri);
@ -200,14 +341,15 @@ public class ReadingListProvider extends SharedBrowserDatabaseProvider {
switch (match) { switch (match) {
case ITEMS_ID: case ITEMS_ID:
trace("Query on ITEMS_ID: " + uri); trace("Query on ITEMS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, ReadingListItems._ID + " = ?"); selection = DBUtils.concatenateWhere(selection, _ID + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) }); new String[] { Long.toString(ContentUris.parseId(uri)) });
case ITEMS: case ITEMS:
trace("Query on ITEMS: " + uri); trace("Query on ITEMS: " + uri);
if (!shouldShowDeleted(uri)) if (!shouldShowDeleted(uri)) {
selection = DBUtils.concatenateWhere(ReadingListItems.IS_DELETED + " = 0", selection); selection = DBUtils.concatenateWhere(IS_DELETED + " = 0", selection);
}
break; break;
default: default:
@ -215,7 +357,7 @@ public class ReadingListProvider extends SharedBrowserDatabaseProvider {
} }
if (TextUtils.isEmpty(sortOrder)) { if (TextUtils.isEmpty(sortOrder)) {
sortOrder = ReadingListItems.DEFAULT_SORT_ORDER; sortOrder = DEFAULT_SORT_ORDER;
} }
trace("Running built query."); trace("Running built query.");
@ -234,14 +376,22 @@ public class ReadingListProvider extends SharedBrowserDatabaseProvider {
switch (match) { switch (match) {
case ITEMS: case ITEMS:
trace("URI is ITEMS: " + uri); trace("URI is ITEMS: " + uri);
return ReadingListItems.CONTENT_TYPE; return CONTENT_TYPE;
case ITEMS_ID: case ITEMS_ID:
trace("URI is ITEMS_ID: " + uri); trace("URI is ITEMS_ID: " + uri);
return ReadingListItems.CONTENT_ITEM_TYPE; return CONTENT_ITEM_TYPE;
} }
debug("URI has unrecognized type: " + uri); debug("URI has unrecognized type: " + uri);
return null; return null;
} }
@Override
protected String getDeletedItemSelection(long earlierThan) {
if (earlierThan == -1L) {
return IS_DELETED + " = 1";
}
return IS_DELETED + " = 1 AND " + CLIENT_LAST_MODIFIED + " <= " + earlierThan;
}
} }

Просмотреть файл

@ -102,9 +102,7 @@ public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDa
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go. // IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis(); final long now = System.currentTimeMillis();
final String selection = SyncColumns.IS_DELETED + " = 1 AND " + final String selection = getDeletedItemSelection(now - MAX_AGE_OF_DELETED_RECORDS);
SyncColumns.DATE_MODIFIED + " <= " +
(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE); final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri)); final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
@ -119,4 +117,12 @@ public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDa
db.delete(tableName, inClause, null); db.delete(tableName, inClause, null);
} }
// Override this, or override cleanUpSomeDeletedRecords.
protected String getDeletedItemSelection(long earlierThan) {
if (earlierThan == -1L) {
return SyncColumns.IS_DELETED + " = 1";
}
return SyncColumns.IS_DELETED + " = 1 AND " + SyncColumns.DATE_MODIFIED + " <= " + earlierThan;
}
} }

Просмотреть файл

@ -48,22 +48,37 @@ class StubReadingListAccessor implements ReadingListAccessor {
} }
@Override @Override
public void addReadingListItem(ContentResolver cr, ContentValues values) { public long addReadingListItem(ContentResolver cr, ContentValues values) {
return 0L;
}
@Override
public long addBasicReadingListItem(ContentResolver cr, String url, String title) {
return 0L;
} }
@Override @Override
public void updateReadingListItem(ContentResolver cr, ContentValues values) { public void updateReadingListItem(ContentResolver cr, ContentValues values) {
} }
@Override @Override
public void removeReadingListItemWithURL(ContentResolver cr, String uri) { public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
} }
@Override @Override
public void registerContentObserver(Context context, ContentObserver observer) { public void registerContentObserver(Context context, ContentObserver observer) {
}
@Override
public void markAsRead(ContentResolver cr, long itemID) {
}
@Override
public void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt) {
}
@Override
public void deleteItem(ContentResolver cr, long itemID) {
} }
} }

Просмотреть файл

@ -10,31 +10,30 @@ import android.content.Context;
import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.db.LocalBrowserDB; import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.db.ReadingListProvider;
import org.mozilla.gecko.overlays.service.ShareData; import org.mozilla.gecko.overlays.service.ShareData;
import static org.mozilla.gecko.db.BrowserContract.Bookmarks; import static org.mozilla.gecko.db.BrowserContract.ReadingListItems;
/** /**
* ShareMethod to add a page to the reading list. * ShareMethod to add a page to the reading list.
* *
* Inserts the given URL/title pair into the reading list database. * Inserts the given URL/title pair into the reading list database.
* TODO: In the event the page turns out not to be reader-mode-compatible, freezes sometimes occur
* when we subsequently load the page in reader mode. (Bug 1044781)
*/ */
public class AddToReadingList extends ShareMethod { public class AddToReadingList extends ShareMethod {
private static final String LOGTAG = "GeckoAddToReadingList"; private static final String LOGTAG = "GeckoAddToReadingList";
@Override @Override
public Result handle(ShareData shareData) { public Result handle(ShareData shareData) {
ContentResolver resolver = context.getContentResolver(); final ContentResolver resolver = context.getContentResolver();
LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE); final ContentValues values = new ContentValues();
values.put(ReadingListItems.TITLE, shareData.title);
values.put(ReadingListItems.URL, shareData.url);
values.put(ReadingListItems.ADDED_ON, System.currentTimeMillis());
values.put(ReadingListItems.ADDED_BY, ReadingListProvider.PLACEHOLDER_THIS_DEVICE);
ContentValues values = new ContentValues(); new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE).getReadingListAccessor().addReadingListItem(resolver, values);
values.put(Bookmarks.TITLE, shareData.title);
values.put(Bookmarks.URL, shareData.url);
browserDB.getReadingListAccessor().addReadingListItem(resolver, values);
return Result.SUCCESS; return Result.SUCCESS;
} }

Просмотреть файл

@ -60,7 +60,7 @@
android:key="sync_now" android:key="sync_now"
android:defaultValue="" android:defaultValue=""
android:persistent="false" android:persistent="false"
android:title="Sync now" android:title="@string/fxaccount_status_sync_now"
android:summary="" /> android:summary="" />
<CheckBoxPreference <CheckBoxPreference

Просмотреть файл

@ -15,13 +15,15 @@ import android.view.TouchDelegate;
import android.view.View; import android.view.View;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import org.mozilla.gecko.BrowserApp.Refreshable;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab; import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs; import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.widget.ThemedImageButton; import org.mozilla.gecko.widget.ThemedImageButton;
import org.mozilla.gecko.widget.ThemedLinearLayout; import org.mozilla.gecko.widget.ThemedLinearLayout;
public class TabStrip extends ThemedLinearLayout { public class TabStrip extends ThemedLinearLayout
implements Refreshable {
private static final String LOGTAG = "GeckoTabStrip"; private static final String LOGTAG = "GeckoTabStrip";
private final TabStripView tabStripView; private final TabStripView tabStripView;
@ -128,6 +130,11 @@ public class TabStrip extends ThemedLinearLayout {
} }
} }
@Override
public void refresh() {
tabStripView.refresh();
}
@Override @Override
public void onLightweightThemeChanged() { public void onLightweightThemeChanged() {
final Drawable drawable = getTheme().getDrawable(this); final Drawable drawable = getTheme().getDrawable(this);

Просмотреть файл

@ -102,7 +102,7 @@ public class TabStripView extends TwoWayView {
updateSelectedStyle(selected); updateSelectedStyle(selected);
if (ensureVisible) { if (ensureVisible) {
ensurePositionIsVisible(selected); ensurePositionIsVisible(selected, true);
} }
} }
} }
@ -239,10 +239,14 @@ public class TabStripView extends TwoWayView {
}); });
} }
private void ensurePositionIsVisible(final int position) { /**
* Ensures the tab at the given position is visible. If we are not restoring tabs and
* shouldAnimate == true, the tab will animate to be visible, if it is not already visible.
*/
private void ensurePositionIsVisible(final int position, final boolean shouldAnimate) {
// We just want to move the strip to the right position // We just want to move the strip to the right position
// when restoring tabs on startup. // when restoring tabs on startup.
if (isRestoringTabs) { if (isRestoringTabs || !shouldAnimate) {
setSelection(position); setSelection(position);
return; return;
} }
@ -414,6 +418,13 @@ public class TabStripView extends TwoWayView {
drawFadingEdge(canvas); drawFadingEdge(canvas);
} }
public void refresh() {
final int selectedPosition = getPositionForSelectedTab();
if (selectedPosition != -1) {
ensurePositionIsVisible(selectedPosition, false);
}
}
private class TabAnimatorListener implements AnimatorListener { private class TabAnimatorListener implements AnimatorListener {
private void setLayerType(int layerType) { private void setLayerType(int layerType) {
final int childCount = getChildCount(); final int childCount = getChildCount();

Просмотреть файл

@ -8,8 +8,12 @@ import java.util.HashSet;
import java.util.Random; import java.util.Random;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import android.app.Activity;
import android.content.ContentResolver;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems; import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.ReadingListAccessor;
import org.mozilla.gecko.db.ReadingListProvider; import org.mozilla.gecko.db.ReadingListProvider;
import android.content.ContentProvider; import android.content.ContentProvider;
@ -19,23 +23,46 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.net.Uri; import android.net.Uri;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.*;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.ADDED_ON;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.CLIENT_LAST_MODIFIED;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.EXCERPT;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.GUID;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.IS_UNREAD;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.RESOLVED_TITLE;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.RESOLVED_URL;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SERVER_LAST_MODIFIED;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SERVER_STORED_ON;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_FLAGS;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_NONE;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_RESOLVED;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_UNREAD_CHANGED;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS_MODIFIED;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS_NEW;
import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS_SYNCED;
import static org.mozilla.gecko.db.BrowserContract.URLColumns.TITLE;
import static org.mozilla.gecko.db.BrowserContract.URLColumns.URL;
public class testReadingListProvider extends ContentProviderTest { public class testReadingListProvider extends ContentProviderTest {
private static final String DB_NAME = "browser.db"; private static final String DB_NAME = "browser.db";
// List of tests to be run sorted by dependency. // List of tests to be run sorted by dependency.
private final TestCase[] TESTS_TO_RUN = { new TestInsertItems(), private final TestCase[] TESTS_TO_RUN = {
new TestDeleteItems(), new TestInsertItems(),
new TestUpdateItems(), new TestDeleteItems(),
new TestBatchOperations(), new TestUpdateItems(),
new TestBrowserProviderNotifications() }; new TestBatchOperations(),
new TestBrowserProviderNotifications(),
new TestStateSequencing(),
};
// Columns used to test for item equivalence. // Columns used to test for item equivalence.
final String[] TEST_COLUMNS = { ReadingListItems.TITLE, final String[] TEST_COLUMNS = { TITLE,
ReadingListItems.URL, URL,
ReadingListItems.EXCERPT, EXCERPT,
ReadingListItems.LENGTH, ADDED_ON };
ReadingListItems.DATE_CREATED };
// Indicates that insertions have been tested. ContentProvider.insert // Indicates that insertions have been tested. ContentProvider.insert
// has been proven to work. // has been proven to work.
@ -64,6 +91,13 @@ public class testReadingListProvider extends ContentProviderTest {
for (TestCase test: TESTS_TO_RUN) { for (TestCase test: TESTS_TO_RUN) {
mTests.add(test); mTests.add(test);
} }
// Disable background fetches of content, because it causes the test harness
// to kill us for network fetches.
Activity a = getActivity();
if (a instanceof BrowserApp) {
((BrowserApp) a).getReadingListHelper().disableBackgroundFetches();
}
} }
public void testReadingListProviderTests() throws Exception { public void testReadingListProviderTests() throws Exception {
@ -85,21 +119,21 @@ public class testReadingListProvider extends ContentProviderTest {
@Override @Override
public void test() throws Exception { public void test() throws Exception {
ContentValues b = createFillerReadingListItem(); ContentValues b = createFillerReadingListItem();
long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, b)); long id = ContentUris.parseId(mProvider.insert(CONTENT_URI, b));
Cursor c = getItemById(id); Cursor c = getItemById(id);
try { try {
mAsserter.ok(c.moveToFirst(), "Inserted item found", ""); mAsserter.ok(c.moveToFirst(), "Inserted item found", "");
assertRowEqualsContentValues(c, b); assertRowEqualsContentValues(c, b);
mAsserter.is(c.getInt(c.getColumnIndex(ReadingListItems.CONTENT_STATUS)), mAsserter.is(c.getInt(c.getColumnIndex(CONTENT_STATUS)),
ReadingListItems.STATUS_UNFETCHED, STATUS_UNFETCHED,
"Inserted item has correct default content status"); "Inserted item has correct default content status");
} finally { } finally {
c.close(); c.close();
} }
testInsertWithNullCol(ReadingListItems.GUID); testInsertWithNullCol(URL);
mContentProviderInsertTested = true; mContentProviderInsertTested = true;
} }
@ -112,7 +146,7 @@ public class testReadingListProvider extends ContentProviderTest {
b.putNull(colName); b.putNull(colName);
try { try {
ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, b)); ContentUris.parseId(mProvider.insert(CONTENT_URI, b));
// If we get to here, the flawed insertion succeeded. Fail the test. // If we get to here, the flawed insertion succeeded. Fail the test.
mAsserter.ok(false, "Insertion did not succeed with " + colName + " == null", ""); mAsserter.ok(false, "Insertion did not succeed with " + colName + " == null", "");
} catch (NullPointerException e) { } catch (NullPointerException e) {
@ -128,17 +162,31 @@ public class testReadingListProvider extends ContentProviderTest {
@Override @Override
public void test() throws Exception { public void test() throws Exception {
long id = insertAnItemWithAssertion(); final long one = insertAnItemWithAssertion();
// Test that the item is only marked as deleted and final long two = insertAnItemWithAssertion();
assignGUID(one);
// Test that the item with a GUID is only marked as deleted and
// not removed from the database. // not removed from the database.
testNonFirefoxSyncDelete(id); testNonSyncDelete(one, true);
// Test that the item is removed from the database. // The item without a GUID is just deleted immediately.
testFirefoxSyncDelete(id); testNonSyncDelete(two, false);
// Test that the item with a GUID is removed from the database when deleted by Sync.
testSyncDelete(one);
final long three = insertAnItemWithAssertion();
id = insertAnItemWithAssertion();
// Test that deleting works with only a URI. // Test that deleting works with only a URI.
testDeleteWithItemURI(id); testDeleteWithItemURI(three);
}
private void assignGUID(final long id) {
final ContentValues values = new ContentValues();
values.put(GUID, "abcdefghi");
mProvider.update(CONTENT_URI, values, _ID + " = " + id, null);
} }
/** /**
@ -146,19 +194,26 @@ public class testReadingListProvider extends ContentProviderTest {
* as deleted and not actually removed from the database. Also verify that the item * as deleted and not actually removed from the database. Also verify that the item
* marked as deleted doesn't show up in a query. * marked as deleted doesn't show up in a query.
* *
* Note that items are deleted immediately if they don't have a GUID.
*
* @param id of the item to be deleted * @param id of the item to be deleted
* @param hasGUID if true, we expect the item to stick around and be marked.
*/ */
private void testNonFirefoxSyncDelete(long id) { private void testNonSyncDelete(long id, boolean hasGUID) {
final int deleted = mProvider.delete(ReadingListItems.CONTENT_URI, final int deleted = mProvider.delete(CONTENT_URI,
ReadingListItems._ID + " = ?", _ID + " = " + id,
new String[] { String.valueOf(id) }); null);
mAsserter.is(deleted, 1, "Inserted item was deleted"); mAsserter.is(deleted, 1, "Inserted item was deleted");
// PARAM_SHOW_DELETED in the URI allows items marked as deleted to be // PARAM_SHOW_DELETED in the URI allows items marked as deleted to be
// included in the query. // included in the query.
Uri uri = appendUriParam(ReadingListItems.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"); Uri uri = appendUriParam(CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1");
assertItemExistsByID(uri, id, "Deleted item was only marked as deleted"); if (hasGUID) {
assertItemExistsByID(uri, id, "Deleted item was only marked as deleted");
} else {
assertItemDoesNotExistByID(uri, id, "Deleted item had no GUID, so was really deleted.");
}
// Test that the 'deleted' item does not show up in a query when PARAM_SHOW_DELETED // Test that the 'deleted' item does not show up in a query when PARAM_SHOW_DELETED
// is not specified in the URI. // is not specified in the URI.
@ -171,14 +226,14 @@ public class testReadingListProvider extends ContentProviderTest {
* *
* @param id of the item to be deleted * @param id of the item to be deleted
*/ */
private void testFirefoxSyncDelete(long id) { private void testSyncDelete(long id) {
final int deleted = mProvider.delete(appendUriParam(ReadingListItems.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), final int deleted = mProvider.delete(appendUriParam(CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
ReadingListItems._ID + " = ?", _ID + " = " + id,
new String[] { String.valueOf(id) }); null);
mAsserter.is(deleted, 1, "Inserted item was deleted"); mAsserter.is(deleted, 1, "Inserted item was deleted");
Uri uri = appendUriParam(ReadingListItems.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"); Uri uri = appendUriParam(CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1");
assertItemDoesNotExistByID(uri, id, "Inserted item is now actually deleted"); assertItemDoesNotExistByID(uri, id, "Inserted item is now actually deleted");
} }
@ -189,7 +244,7 @@ public class testReadingListProvider extends ContentProviderTest {
* @param id of the item to be deleted * @param id of the item to be deleted
*/ */
private void testDeleteWithItemURI(long id) { private void testDeleteWithItemURI(long id) {
final int deleted = mProvider.delete(ContentUris.withAppendedId(ReadingListItems.CONTENT_URI, id), null, null); final int deleted = mProvider.delete(ContentUris.withAppendedId(CONTENT_URI, id), null, null);
mAsserter.is(deleted, 1, "Inserted item was deleted using URI with id"); mAsserter.is(deleted, 1, "Inserted item was deleted using URI with id");
} }
} }
@ -205,7 +260,7 @@ public class testReadingListProvider extends ContentProviderTest {
ensureCanInsert(); ensureCanInsert();
ContentValues original = createFillerReadingListItem(); ContentValues original = createFillerReadingListItem();
long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, original)); long id = ContentUris.parseId(mProvider.insert(CONTENT_URI, original));
int updated = 0; int updated = 0;
Long originalDateCreated = null; Long originalDateCreated = null;
Long originalDateModified = null; Long originalDateModified = null;
@ -214,16 +269,16 @@ public class testReadingListProvider extends ContentProviderTest {
try { try {
mAsserter.ok(c.moveToFirst(), "Inserted item found", ""); mAsserter.ok(c.moveToFirst(), "Inserted item found", "");
originalDateCreated = c.getLong(c.getColumnIndex(ReadingListItems.DATE_CREATED)); originalDateCreated = c.getLong(c.getColumnIndex(ADDED_ON));
originalDateModified = c.getLong(c.getColumnIndex(ReadingListItems.DATE_MODIFIED)); originalDateModified = c.getLong(c.getColumnIndex(CLIENT_LAST_MODIFIED));
updates.put(ReadingListItems.TITLE, original.getAsString(ReadingListItems.TITLE) + "CHANGED"); updates.put(TITLE, original.getAsString(TITLE) + "CHANGED");
updates.put(ReadingListItems.URL, original.getAsString(ReadingListItems.URL) + "/more/stuff"); updates.put(URL, original.getAsString(URL) + "/more/stuff");
updates.put(ReadingListItems.EXCERPT, original.getAsString(ReadingListItems.EXCERPT) + "CHANGED"); updates.put(EXCERPT, original.getAsString(EXCERPT) + "CHANGED");
updated = mProvider.update(ReadingListItems.CONTENT_URI, updates, updated = mProvider.update(CONTENT_URI, updates,
ReadingListItems._ID + " = ?", _ID + " = ?",
new String[] { String.valueOf(id) }); new String[] { String.valueOf(id) });
mAsserter.is(updated, 1, "Inserted item was updated"); mAsserter.is(updated, 1, "Inserted item was updated");
} finally { } finally {
@ -232,18 +287,17 @@ public class testReadingListProvider extends ContentProviderTest {
// Name change for clarity. These values will be compared with the // Name change for clarity. These values will be compared with the
// current cursor row. // current cursor row.
ContentValues expectedValues = updates; final ContentValues expectedValues = updates;
c = getItemById(id); c = getItemById(id);
try { try {
mAsserter.ok(c.moveToFirst(), "Updated item found", ""); mAsserter.ok(c.moveToFirst(), "Updated item found", "");
mAsserter.isnot(c.getLong(c.getColumnIndex(ReadingListItems.DATE_MODIFIED)), mAsserter.isnot(c.getLong(c.getColumnIndex(CLIENT_LAST_MODIFIED)),
originalDateModified, originalDateModified,
"Date modified should have changed"); "Date modified should have changed");
// DATE_CREATED and LENGTH should equal old values since they weren't updated. // ADDED_ON shouldn't have changed.
expectedValues.put(ReadingListItems.DATE_CREATED, originalDateCreated); expectedValues.put(ADDED_ON, originalDateCreated);
expectedValues.put(ReadingListItems.LENGTH, original.getAsString(ReadingListItems.LENGTH)); assertRowEqualsContentValues(c, expectedValues, /* compareDateModified */ false, TEST_COLUMNS);
assertRowEqualsContentValues(c, expectedValues, /* compareDateModified */ false);
} finally { } finally {
c.close(); c.close();
} }
@ -251,44 +305,24 @@ public class testReadingListProvider extends ContentProviderTest {
// Test that updates on an item that doesn't exist does not modify any rows. // Test that updates on an item that doesn't exist does not modify any rows.
testUpdateWithInvalidID(); testUpdateWithInvalidID();
// Test that update fails when a GUID is null.
testUpdateWithNullCol(id, ReadingListItems.GUID);
mContentProviderUpdateTested = true; mContentProviderUpdateTested = true;
} }
/** /**
* Test that updates on an item that doesn't exist does * Test that updates on an item that doesn't exist does
* not modify any rows. * not modify any rows.
*
* @param id of the item to be deleted
*/ */
private void testUpdateWithInvalidID() { private void testUpdateWithInvalidID() {
ensureEmptyDatabase(); ensureEmptyDatabase();
final ContentValues b = createFillerReadingListItem(); final ContentValues b = createFillerReadingListItem();
final long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, b)); final long id = ContentUris.parseId(mProvider.insert(CONTENT_URI, b));
final long INVALID_ID = id + 1; final long INVALID_ID = id + 1;
final ContentValues updates = new ContentValues(); final ContentValues updates = new ContentValues();
updates.put(ReadingListItems.TITLE, b.getAsString(ReadingListItems.TITLE) + "CHANGED"); updates.put(TITLE, b.getAsString(TITLE) + "CHANGED");
final int updated = mProvider.update(ReadingListItems.CONTENT_URI, updates, final int updated = mProvider.update(CONTENT_URI, updates,
ReadingListItems._ID + " = ?", _ID + " = ?",
new String[] { String.valueOf(INVALID_ID) }); new String[] { String.valueOf(INVALID_ID) });
mAsserter.is(updated, 0, "Should not be able to update item with an invalid GUID"); mAsserter.is(updated, 0, "Should not be able to update item with an invalid ID");
}
/**
* Test that update fails when a required column is null.
*/
private int testUpdateWithNullCol(long id, String colName) {
ContentValues updates = new ContentValues();
updates.putNull(colName);
int updated = mProvider.update(ReadingListItems.CONTENT_URI, updates,
ReadingListItems._ID + " = ?",
new String[] { String.valueOf(id) });
mAsserter.is(updated, 0, "Should not be able to update item with " + colName + " == null ");
return updated;
} }
} }
@ -306,23 +340,22 @@ public class testReadingListProvider extends ContentProviderTest {
for (int i = 0; i < ITEM_COUNT; i++) { for (int i = 0; i < ITEM_COUNT; i++) {
final String url = "http://www.test.org/" + i; final String url = "http://www.test.org/" + i;
allVals[i] = new ContentValues(); allVals[i] = new ContentValues();
allVals[i].put(ReadingListItems.TITLE, "Test" + i); allVals[i].put(TITLE, "Test" + i);
allVals[i].put(ReadingListItems.URL, url); allVals[i].put(URL, url);
allVals[i].put(ReadingListItems.EXCERPT, "EXCERPT" + i); allVals[i].put(EXCERPT, "EXCERPT" + i);
allVals[i].put(ReadingListItems.LENGTH, i);
urls.add(url); urls.add(url);
} }
int inserts = mProvider.bulkInsert(ReadingListItems.CONTENT_URI, allVals); int inserts = mProvider.bulkInsert(CONTENT_URI, allVals);
mAsserter.is(inserts, ITEM_COUNT, "Excepted number of inserts matches"); mAsserter.is(inserts, ITEM_COUNT, "Excepted number of inserts matches");
Cursor c = mProvider.query(ReadingListItems.CONTENT_URI, null, final Cursor c = mProvider.query(CONTENT_URI, null,
null, null,
null, null,
null); null);
try { try {
while (c.moveToNext()) { while (c.moveToNext()) {
final String url = c.getString(c.getColumnIndex(ReadingListItems.URL)); final String url = c.getString(c.getColumnIndex(URL));
mAsserter.ok(urls.contains(url), "Bulk inserted item with url == " + url + " was found in the DB", ""); mAsserter.ok(urls.contains(url), "Bulk inserted item with url == " + url + " was found in the DB", "");
// We should only be seeing each item once. Remove from set to prevent dups. // We should only be seeing each item once. Remove from set to prevent dups.
urls.remove(url); urls.remove(url);
@ -369,10 +402,10 @@ public class testReadingListProvider extends ContentProviderTest {
// Update // Update
mResolver.notifyChangeList.clear(); mResolver.notifyChangeList.clear();
h.put(ReadingListItems.TITLE, "http://newexample.com"); h.put(TITLE, "http://newexample.com");
long numUpdated = mProvider.update(ReadingListItems.CONTENT_URI, h, long numUpdated = mProvider.update(ReadingListItems.CONTENT_URI, h,
ReadingListItems._ID + " = ?", _ID + " = ?",
new String[] { String.valueOf(id) }); new String[] { String.valueOf(id) });
mAsserter.is(numUpdated, mAsserter.is(numUpdated,
@ -423,12 +456,198 @@ public class testReadingListProvider extends ContentProviderTest {
} }
} }
private class TestStateSequencing extends TestCase {
@Override
protected void test() throws Exception {
final ReadingListAccessor accessor = getTestProfile().getDB().getReadingListAccessor();
final ContentResolver cr = getActivity().getContentResolver();
final Uri syncURI = CONTENT_URI.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1")
.appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
.build();
mAsserter.ok(accessor != null, "We have an accessor.", null);
// Verify that the accessor thinks we're empty.
mAsserter.ok(0 == accessor.getCount(cr), "We have no items.", null);
// Insert an item via the accessor.
final long addedItem = accessor.addBasicReadingListItem(cr, "http://example.org/", "Example A");
mAsserter.ok(1 == accessor.getCount(cr), "We have one item.", null);
final Cursor cursor = accessor.getReadingList(cr);
try {
mAsserter.ok(cursor.moveToNext(), "The cursor isn't empty.", null);
mAsserter.ok(1 == cursor.getCount(), "The cursor agrees.", null);
} finally {
cursor.close();
}
// Verify that it has no GUID, that its state is NEW, etc.
// This requires fetching more fields than the accessor uses.
final Cursor all = getEverything(syncURI);
try {
mAsserter.ok(all.moveToNext(), "The cursor isn't empty.", null);
mAsserter.ok(1 == all.getCount(), "The cursor agrees.", null);
ContentValues expected = new ContentValues();
expected.putNull(GUID);
expected.put(SYNC_STATUS, SYNC_STATUS_NEW);
expected.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
expected.put(URL, "http://example.org/");
expected.put(TITLE, "Example A");
expected.putNull(RESOLVED_URL);
expected.putNull(RESOLVED_TITLE);
final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, URL, TITLE, RESOLVED_URL, RESOLVED_TITLE, URL, TITLE};
assertRowEqualsContentValues(all, expected, false, testColumns);
} finally {
all.close();
}
// Pretend that it was just synced.
final long serverTime = System.currentTimeMillis();
final ContentValues wasSynced = new ContentValues();
wasSynced.put(GUID, "eeeeeeeeeeeeee");
wasSynced.put(SYNC_STATUS, SYNC_STATUS_SYNCED);
wasSynced.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
wasSynced.put(SERVER_STORED_ON, serverTime);
wasSynced.put(SERVER_LAST_MODIFIED, serverTime);
mAsserter.ok(1 == mProvider.update(syncURI, wasSynced, _ID + " = " + addedItem, null), "Updated one item.", null);
final Cursor afterSync = getEverything(syncURI);
try {
mAsserter.ok(afterSync.moveToNext(), "The cursor isn't empty.", null);
final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, SERVER_STORED_ON, SERVER_LAST_MODIFIED};
assertRowEqualsContentValues(afterSync, wasSynced, false, testColumns);
} finally {
afterSync.close();
}
// Make changes to the record that exercise the various change flags, verifying that
// the correct flags are set.
final long beforeMarkedRead = System.currentTimeMillis();
accessor.markAsRead(cr, addedItem);
final ContentValues markedAsRead = new ContentValues();
markedAsRead.put(GUID, "eeeeeeeeeeeeee");
markedAsRead.put(SYNC_STATUS, SYNC_STATUS_MODIFIED);
markedAsRead.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_UNREAD_CHANGED);
markedAsRead.put(IS_UNREAD, 0);
final Cursor afterMarkedRead = getEverything(syncURI);
try {
mAsserter.ok(afterMarkedRead.moveToNext(), "The cursor isn't empty.", null);
final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, IS_UNREAD};
assertRowEqualsContentValues(afterMarkedRead, markedAsRead, false, testColumns);
assertModifiedInRange(afterMarkedRead, beforeMarkedRead);
} finally {
afterMarkedRead.close();
}
// Now our content is here!
final long beforeContentUpdated = System.currentTimeMillis();
accessor.updateContent(cr, addedItem, "New title", "http://www.example.com/article", "The excerpt is long.");
// After this the content status should have changed, and we should be flagged to sync.
final ContentValues contentUpdated = new ContentValues();
contentUpdated.put(GUID, "eeeeeeeeeeeeee");
contentUpdated.put(SYNC_STATUS, SYNC_STATUS_MODIFIED);
contentUpdated.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_UNREAD_CHANGED | SYNC_CHANGE_RESOLVED);
contentUpdated.put(IS_UNREAD, 0);
contentUpdated.put(CONTENT_STATUS, STATUS_FETCHED_ARTICLE);
final Cursor afterContentUpdated = getEverything(syncURI);
try {
mAsserter.ok(afterContentUpdated.moveToNext(), "The cursor isn't empty.", null);
final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, IS_UNREAD, CONTENT_STATUS};
assertRowEqualsContentValues(afterContentUpdated, contentUpdated, false, testColumns);
assertModifiedInRange(afterContentUpdated, beforeContentUpdated);
} finally {
afterContentUpdated.close();
}
// Delete the record, and verify that its Sync state is DELETED.
final long beforeDeletion = System.currentTimeMillis();
accessor.deleteItem(cr, addedItem);
final ContentValues itemDeleted = new ContentValues();
itemDeleted.put(GUID, "eeeeeeeeeeeeee");
itemDeleted.put(SYNC_STATUS, SYNC_STATUS_DELETED);
itemDeleted.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
// TODO: CONTENT_STATUS on deletion?
final Cursor afterDeletion = getEverything(syncURI);
try {
mAsserter.ok(afterDeletion.moveToNext(), "The cursor isn't empty.", null);
final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS};
assertRowEqualsContentValues(afterDeletion, itemDeleted, false, testColumns);
assertModifiedInRange(afterDeletion, beforeDeletion);
} finally {
afterDeletion.close();
}
// The accessor will no longer return the record.
mAsserter.ok(0 == accessor.getCount(cr), "No items found.", null);
// Add a new record as Sync -- it should start in state SYNCED.
final ContentValues newRecord = new ContentValues();
final long newServerTime = System.currentTimeMillis() - 50000;
newRecord.put(GUID, "ffeeeeeeeeeeee");
newRecord.put(SYNC_STATUS, SYNC_STATUS_SYNCED);
newRecord.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
newRecord.put(SERVER_STORED_ON, newServerTime);
newRecord.put(SERVER_LAST_MODIFIED, newServerTime);
newRecord.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
newRecord.put(URL, "http://www.mozilla.org/");
newRecord.put(TITLE, "Mozilla");
final long newID = ContentUris.parseId(cr.insert(syncURI, newRecord));
mAsserter.ok(newID > 0, "New ID is greater than 0.", null);
mAsserter.ok(newID != addedItem, "New ID differs from last ID.", null);
final Cursor afterNewInsert = getEverything(syncURI);
try {
mAsserter.ok(afterNewInsert.moveToNext(), "The cursor isn't empty.", null);
mAsserter.ok(2 == afterNewInsert.getCount(), "The cursor has two rows.", null);
// Default sort order means newest first.
final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, SERVER_STORED_ON, SERVER_LAST_MODIFIED, CLIENT_LAST_MODIFIED, URL, TITLE};
assertRowEqualsContentValues(afterNewInsert, newRecord, false, testColumns);
} finally {
afterNewInsert.close();
}
// Make a change to it. Verify that it's now changed with the right flags.
final long beforeNewRead = System.currentTimeMillis();
accessor.markAsRead(cr, newID);
newRecord.put(SYNC_STATUS, SYNC_STATUS_MODIFIED);
newRecord.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_UNREAD_CHANGED);
final Cursor afterNewRead = getEverything(syncURI);
try {
mAsserter.ok(afterNewRead.moveToNext(), "The cursor isn't empty.", null);
mAsserter.ok(2 == afterNewRead.getCount(), "The cursor has two rows.", null);
// Default sort order means newest first.
final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, SERVER_STORED_ON, SERVER_LAST_MODIFIED, URL, TITLE};
assertRowEqualsContentValues(afterNewRead, newRecord, false, testColumns);
assertModifiedInRange(afterNewRead, beforeNewRead);
} finally {
afterNewRead.close();
}
}
private void assertModifiedInRange(Cursor cursor, long earliest) {
final long dbModified = cursor.getLong(cursor.getColumnIndexOrThrow(CLIENT_LAST_MODIFIED));
mAsserter.ok(dbModified >= earliest, "DB timestamp is at least as late as earliest.", null);
mAsserter.ok(dbModified <= System.currentTimeMillis(), "DB timestamp is earlier than now.", null);
}
}
/** /**
* Removes all items from the DB. * Removes all items from the DB.
*/ */
private void ensureEmptyDatabase() { private void ensureEmptyDatabase() {
Uri uri = appendUriParam(ReadingListItems.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"); getWritableDatabase(CONTENT_URI).delete(TABLE_NAME, null, null);
getWritableDatabase(uri).delete(ReadingListItems.TABLE_NAME, null, null);
} }
@ -443,34 +662,35 @@ public class testReadingListProvider extends ContentProviderTest {
* Checks that the values in the cursor's current row match those * Checks that the values in the cursor's current row match those
* in the ContentValues object. * in the ContentValues object.
* *
* @param cursor over the row to be checked * @param testColumns
* @param values to be checked * @param cursorWithActual over the row to be checked
* @param expectedValues to be checked
*/ */
private void assertRowEqualsContentValues(Cursor cursorWithActual, ContentValues expectedValues, boolean compareDateModified) { private void assertRowEqualsContentValues(Cursor cursorWithActual, ContentValues expectedValues, boolean compareDateModified, String[] testColumns) {
for (String column: TEST_COLUMNS) { for (String column: testColumns) {
String expected = expectedValues.getAsString(column); String expected = expectedValues.getAsString(column);
String actual = cursorWithActual.getString(cursorWithActual.getColumnIndex(column)); String actual = cursorWithActual.getString(cursorWithActual.getColumnIndex(column));
mAsserter.is(actual, expected, "Item has correct " + column); mAsserter.is(actual, expected, "Item has correct " + column);
} }
if (compareDateModified) { if (compareDateModified) {
String expected = expectedValues.getAsString(ReadingListItems.DATE_MODIFIED); String expected = expectedValues.getAsString(CLIENT_LAST_MODIFIED);
String actual = cursorWithActual.getString(cursorWithActual.getColumnIndex(ReadingListItems.DATE_MODIFIED)); String actual = cursorWithActual.getString(cursorWithActual.getColumnIndex(CLIENT_LAST_MODIFIED));
mAsserter.is(actual, expected, "Item has correct " + ReadingListItems.DATE_MODIFIED); mAsserter.is(actual, expected, "Item has correct " + CLIENT_LAST_MODIFIED);
} }
} }
private void assertRowEqualsContentValues(Cursor cursorWithActual, ContentValues expectedValues) { private void assertRowEqualsContentValues(Cursor cursorWithActual, ContentValues expectedValues) {
assertRowEqualsContentValues(cursorWithActual, expectedValues, true); assertRowEqualsContentValues(cursorWithActual, expectedValues, true, TEST_COLUMNS);
} }
private ContentValues fillContentValues(String title, String url, String excerpt) { private ContentValues fillContentValues(String title, String url, String excerpt) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(ReadingListItems.TITLE, title); values.put(TITLE, title);
values.put(ReadingListItems.URL, url); values.put(URL, url);
values.put(ReadingListItems.EXCERPT, excerpt); values.put(EXCERPT, excerpt);
values.put(ReadingListItems.LENGTH, excerpt.length()); values.put(ADDED_ON, System.currentTimeMillis());
return values; return values;
} }
@ -480,15 +700,23 @@ public class testReadingListProvider extends ContentProviderTest {
return fillContentValues("Example", "http://example.com/?num=" + rand.nextInt(), "foo bar"); return fillContentValues("Example", "http://example.com/?num=" + rand.nextInt(), "foo bar");
} }
private Cursor getEverything(Uri uri) {
return mProvider.query(uri,
null,
null,
null,
null);
}
private Cursor getItemById(Uri uri, long id, String[] projection) { private Cursor getItemById(Uri uri, long id, String[] projection) {
return mProvider.query(uri, projection, return mProvider.query(uri, projection,
ReadingListItems._ID + " = ?", _ID + " = ?",
new String[] { String.valueOf(id) }, new String[] { String.valueOf(id) },
null); null);
} }
private Cursor getItemById(long id) { private Cursor getItemById(long id) {
return getItemById(ReadingListItems.CONTENT_URI, id, null); return getItemById(CONTENT_URI, id, null);
} }
private Cursor getItemById(Uri uri, long id) { private Cursor getItemById(Uri uri, long id) {
@ -518,7 +746,7 @@ public class testReadingListProvider extends ContentProviderTest {
ensureCanInsert(); ensureCanInsert();
ContentValues v = createFillerReadingListItem(); ContentValues v = createFillerReadingListItem();
long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, v)); long id = ContentUris.parseId(mProvider.insert(CONTENT_URI, v));
assertItemExistsByID(id, "Inserted item found"); assertItemExistsByID(id, "Inserted item found");
return id; return id;

Просмотреть файл

@ -88,10 +88,10 @@ BreakpointActorMap.prototype = {
* Generate all BreakpointActors that match the given location in * Generate all BreakpointActors that match the given location in
* this BreakpointActorMap. * this BreakpointActorMap.
* *
* @param GeneratedLocation location * @param OriginalLocation location
* The location for which matching BreakpointActors should be generated. * The location for which matching BreakpointActors should be generated.
*/ */
findActors: function* (location = new GeneratedLocation()) { findActors: function* (location = new OriginalLocation()) {
function* findKeys(object, key) { function* findKeys(object, key) {
if (key !== undefined) { if (key !== undefined) {
if (key in object) { if (key in object) {
@ -106,12 +106,21 @@ BreakpointActorMap.prototype = {
} }
let query = { let query = {
sourceActorID: location.generatedSourceActor ? location.generatedSourceActor.actorID : undefined, sourceActorID: location.originalSourceActor ? location.originalSourceActor.actorID : undefined,
line: location.generatedLine, line: location.originalLine,
beginColumn: location.generatedColumn ? location.generatedColumn : undefined,
endColumn: location.generatedColumn ? location.generatedColumn + 1 : undefined
}; };
// If location contains a line, assume we are searching for a whole line
// breakpoint, and set begin/endColumn accordingly. Otherwise, we are
// searching for all breakpoints, so begin/endColumn should be left unset.
if (location.originalLine) {
query.beginColumn = location.originalColumn ? location.originalColumn : 0;
query.endColumn = location.originalColumn ? location.originalColumn + 1 : Infinity;
} else {
query.beginColumn = location.originalColumn ? query.originalColumn : undefined;
query.endColumn = location.originalColumn ? query.originalColumn + 1 : undefined;
}
for (let sourceActorID of findKeys(this._actors, query.sourceActorID)) for (let sourceActorID of findKeys(this._actors, query.sourceActorID))
for (let line of findKeys(this._actors[sourceActorID], query.line)) for (let line of findKeys(this._actors[sourceActorID], query.line))
for (let beginColumn of findKeys(this._actors[sourceActorID][line], query.beginColumn)) for (let beginColumn of findKeys(this._actors[sourceActorID][line], query.beginColumn))
@ -124,14 +133,14 @@ BreakpointActorMap.prototype = {
* Return the BreakpointActor at the given location in this * Return the BreakpointActor at the given location in this
* BreakpointActorMap. * BreakpointActorMap.
* *
* @param GeneratedLocation location * @param OriginalLocation location
* The location for which the BreakpointActor should be returned. * The location for which the BreakpointActor should be returned.
* *
* @returns BreakpointActor actor * @returns BreakpointActor actor
* The BreakpointActor at the given location. * The BreakpointActor at the given location.
*/ */
getActor: function (location) { getActor: function (originalLocation) {
for (let actor of this.findActors(location)) { for (let actor of this.findActors(originalLocation)) {
return actor; return actor;
} }
@ -142,19 +151,19 @@ BreakpointActorMap.prototype = {
* Set the given BreakpointActor to the given location in this * Set the given BreakpointActor to the given location in this
* BreakpointActorMap. * BreakpointActorMap.
* *
* @param GeneratedLocation location * @param OriginalLocation location
* The location to which the given BreakpointActor should be set. * The location to which the given BreakpointActor should be set.
* *
* @param BreakpointActor actor * @param BreakpointActor actor
* The BreakpointActor to be set to the given location. * The BreakpointActor to be set to the given location.
*/ */
setActor: function (location, actor) { setActor: function (location, actor) {
let { generatedSourceActor, generatedLine, generatedColumn } = location; let { originalSourceActor, originalLine, originalColumn } = location;
let sourceActorID = generatedSourceActor.actorID; let sourceActorID = originalSourceActor.actorID;
let line = generatedLine; let line = originalLine;
let beginColumn = generatedColumn ? generatedColumn : 0; let beginColumn = originalColumn ? originalColumn : 0;
let endColumn = generatedColumn ? generatedColumn + 1 : Infinity; let endColumn = originalColumn ? originalColumn + 1 : Infinity;
if (!this._actors[sourceActorID]) { if (!this._actors[sourceActorID]) {
this._actors[sourceActorID] = []; this._actors[sourceActorID] = [];
@ -175,16 +184,16 @@ BreakpointActorMap.prototype = {
* Delete the BreakpointActor from the given location in this * Delete the BreakpointActor from the given location in this
* BreakpointActorMap. * BreakpointActorMap.
* *
* @param GeneratedLocation location * @param OriginalLocation location
* The location from which the BreakpointActor should be deleted. * The location from which the BreakpointActor should be deleted.
*/ */
deleteActor: function (location) { deleteActor: function (location) {
let { generatedSourceActor, generatedLine, generatedColumn } = location; let { originalSourceActor, originalLine, originalColumn } = location;
let sourceActorID = generatedSourceActor.actorID; let sourceActorID = originalSourceActor.actorID;
let line = generatedLine; let line = originalLine;
let beginColumn = generatedColumn ? generatedColumn : 0; let beginColumn = originalColumn ? originalColumn : 0;
let endColumn = generatedColumn ? generatedColumn + 1 : Infinity; let endColumn = originalColumn ? originalColumn + 1 : Infinity;
if (this._actors[sourceActorID]) { if (this._actors[sourceActorID]) {
if (this._actors[sourceActorID][line]) { if (this._actors[sourceActorID][line]) {
@ -2027,14 +2036,23 @@ ThreadActor.prototype = {
} }
// Set any stored breakpoints. // Set any stored breakpoints.
let promises = [];
let sourceActor = this.sources.createNonSourceMappedActor(aScript.source);
let endLine = aScript.startLine + aScript.lineCount - 1; let endLine = aScript.startLine + aScript.lineCount - 1;
let source = this.sources.createNonSourceMappedActor(aScript.source); for (let actor of this.breakpointActorMap.findActors()) {
for (let bpActor of this.breakpointActorMap.findActors({ sourceActor: source })) { promises.push(this.sources.getGeneratedLocation(actor.originalLocation)
// Limit the search to the line numbers contained in the new script. .then((generatedLocation) => {
if (bpActor.generatedLocation.generatedLine >= aScript.startLine // Limit the search to the line numbers contained in the new script.
&& bpActor.generatedLocation.generatedLine <= endLine) { if (generatedLocation.generatedSourceActor.actorID === sourceActor.actorID &&
source.setBreakpointForActor(bpActor); generatedLocation.generatedLine >= aScript.startLine &&
} generatedLocation.generatedLine <= endLine) {
sourceActor.setBreakpointForActor(actor, generatedLocation);
}
}));
}
if (promises.length > 0) {
this.synchronize(Promise.all(promises));
} }
// Go ahead and establish the source actors for this script, which // Go ahead and establish the source actors for this script, which
@ -2709,21 +2727,21 @@ SourceActor.prototype = {
* NB: This will override a pre-existing BreakpointActor's condition with * NB: This will override a pre-existing BreakpointActor's condition with
* the given the location's condition. * the given the location's condition.
* *
* @param Object originalLocation * @param OriginalLocation originalLocation
* The original location of the breakpoint. * The original location of the breakpoint.
* @param Object generatedLocation * @param GeneratedLocation generatedLocation
* The generated location of the breakpoint. * The generated location of the breakpoint.
* @returns BreakpointActor * @returns BreakpointActor
*/ */
_getOrCreateBreakpointActor: function (originalLocation, generatedLocation, _getOrCreateBreakpointActor: function (originalLocation, generatedLocation,
condition) condition)
{ {
let actor = this.breakpointActorMap.getActor(generatedLocation); let actor = this.breakpointActorMap.getActor(originalLocation);
if (!actor) { if (!actor) {
actor = new BreakpointActor(this.threadActor, originalLocation, actor = new BreakpointActor(this.threadActor, originalLocation,
generatedLocation, condition); generatedLocation, condition);
this.threadActor.threadLifetimePool.addActor(actor); this.threadActor.threadLifetimePool.addActor(actor);
this.breakpointActorMap.setActor(generatedLocation, actor); this.breakpointActorMap.setActor(originalLocation, actor);
return actor; return actor;
} }
@ -2832,26 +2850,24 @@ SourceActor.prototype = {
let actor = this._getOrCreateBreakpointActor(originalLocation, let actor = this._getOrCreateBreakpointActor(originalLocation,
generatedLocation, generatedLocation,
condition); condition);
return generatedLocation.generatedSourceActor.setBreakpointForActor(actor); return generatedLocation.generatedSourceActor
.setBreakpointForActor(actor, generatedLocation);
}); });
}, },
/* /*
* Set the given BreakpointActor as breakpoint handler on all scripts that * Ensure the given BreakpointActor is set as breakpoint handler on all
* match the given location for which the BreakpointActor is not already a * scripts that match the given generated location.
* breakpoint handler.
* *
* @param BreakpointActor actor * @param BreakpointActor actor
* The BreakpointActor to set as breakpoint handler. * The BreakpointActor to be set as breakpoint handler for the given
* generated location.
* @param GeneratedLocation generatedLocation
* The generated location for which the BreakpointActor should be set
* as breakpoint handler.
*/ */
setBreakpointForActor: function (actor) { setBreakpointForActor: function (actor, generatedLocation) {
let originalLocation = actor.originalLocation; let originalLocation = actor.originalLocation;
let generatedLocation = new GeneratedLocation(
this,
actor.generatedLocation.generatedLine,
actor.generatedLocation.generatedColumn
);
let { generatedLine, generatedColumn } = generatedLocation; let { generatedLine, generatedColumn } = generatedLocation;
// Find all scripts matching the given location. We will almost always have // Find all scripts matching the given location. We will almost always have
@ -2919,27 +2935,11 @@ SourceActor.prototype = {
result.line, result.line,
generatedLocation.generatedColumn generatedLocation.generatedColumn
); );
// Check whether we already have a breakpoint actor for the actual
// location. If we do have an existing actor, then the actor we created
// above is redundant and must be destroyed. If we do not have an existing
// actor, we need to update the breakpoint store with the new location.
let existingActor = this.breakpointActorMap.getActor(actualGeneratedLocation);
if (existingActor) {
actor.onDelete();
this.breakpointActorMap.deleteActor(generatedLocation);
actor = existingActor;
} else {
actor.generatedLocation = actualGeneratedLocation;
this.breakpointActorMap.deleteActor(generatedLocation);
this.breakpointActorMap.setActor(actualGeneratedLocation, actor);
setBreakpointOnEntryPoints(this.threadActor, actor, result.entryPoints);
}
} else { } else {
setBreakpointOnEntryPoints(this.threadActor, actor, result.entryPoints);
actualGeneratedLocation = generatedLocation; actualGeneratedLocation = generatedLocation;
} }
setBreakpointOnEntryPoints(this.threadActor, actor, result.entryPoints);
} }
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
@ -2953,6 +2953,22 @@ SourceActor.prototype = {
if (actualOriginalLocation.originalSourceActor.url !== originalLocation.originalSourceActor.url || if (actualOriginalLocation.originalSourceActor.url !== originalLocation.originalSourceActor.url ||
actualOriginalLocation.originalLine !== originalLocation.originalLine) actualOriginalLocation.originalLine !== originalLocation.originalLine)
{ {
// Check whether we already have a breakpoint actor for the actual
// location. If we do have an existing actor, then the actor we created
// above is redundant and must be destroyed. If we do not have an existing
// actor, we need to update the breakpoint store with the new location.
let existingActor = this.breakpointActorMap.getActor(actualOriginalLocation);
if (existingActor) {
actor.onDelete();
this.breakpointActorMap.deleteActor(originalLocation);
response.actor = existingActor.actorID;
} else {
actor.generatedLocation = actualGeneratedLocation;
this.breakpointActorMap.deleteActor(originalLocation);
this.breakpointActorMap.setActor(actualOriginalLocation, actor);
}
response.actualLocation = { response.actualLocation = {
source: actualOriginalLocation.originalSourceActor.form(), source: actualOriginalLocation.originalSourceActor.form(),
line: actualOriginalLocation.originalLine, line: actualOriginalLocation.originalLine,
@ -4612,16 +4628,10 @@ FrameActor.prototype.requestTypes = {
* *
* @param ThreadActor aThreadActor * @param ThreadActor aThreadActor
* The parent thread actor that contains this breakpoint. * The parent thread actor that contains this breakpoint.
* @param object aOriginalLocation * @param OriginalLocation originalLocation
* An object with the following properties: * The original location of the breakpoint.
* - sourceActor: A SourceActor that represents the source * @param GeneratedLocation generatedLocation
* - line: the specified line * The generated location of the breakpoint.
* - column: the specified column
* @param object aGeneratedLocation
* An object with the following properties:
* - sourceActor: A SourceActor that represents the source
* - line: the specified line
* - column: the specified column
* @param string aCondition * @param string aCondition
* Optional. A condition which, when false, will cause the breakpoint to * Optional. A condition which, when false, will cause the breakpoint to
* be skipped. * be skipped.
@ -4723,8 +4733,8 @@ BreakpointActor.prototype = {
*/ */
onDelete: function (aRequest) { onDelete: function (aRequest) {
// Remove from the breakpoint store. // Remove from the breakpoint store.
if (this.generatedLocation) { if (this.originalLocation) {
this.threadActor.breakpointActorMap.deleteActor(this.generatedLocation); this.threadActor.breakpointActorMap.deleteActor(this.originalLocation);
} }
this.threadActor.threadLifetimePool.removeActor(this); this.threadActor.threadLifetimePool.removeActor(this);
// Remove the actual breakpoint from the associated scripts. // Remove the actual breakpoint from the associated scripts.

Просмотреть файл

@ -21,13 +21,13 @@ function run_test()
function test_get_actor() { function test_get_actor() {
let bpStore = new BreakpointActorMap(); let bpStore = new BreakpointActorMap();
let location = { let location = {
generatedSourceActor: { actor: 'actor1' }, originalSourceActor: { actor: 'actor1' },
generatedLine: 3 originalLine: 3
}; };
let columnLocation = { let columnLocation = {
generatedSourceActor: { actor: 'actor2' }, originalSourceActor: { actor: 'actor2' },
generatedLine: 5, originalLine: 5,
generatedColumn: 15 originalColumn: 15
}; };
// Shouldn't have breakpoint // Shouldn't have breakpoint
@ -59,9 +59,9 @@ function test_set_actor() {
// Breakpoint with column // Breakpoint with column
let bpStore = new BreakpointActorMap(); let bpStore = new BreakpointActorMap();
let location = { let location = {
generatedSourceActor: { actor: 'actor1' }, originalSourceActor: { actor: 'actor1' },
generatedLine: 10, originalLine: 10,
generatedColumn: 9 originalColumn: 9
}; };
bpStore.setActor(location, {}); bpStore.setActor(location, {});
do_check_true(!!bpStore.getActor(location), do_check_true(!!bpStore.getActor(location),
@ -69,8 +69,8 @@ function test_set_actor() {
// Breakpoint without column (whole line breakpoint) // Breakpoint without column (whole line breakpoint)
location = { location = {
generatedSourceActor: { actor: 'actor2' }, originalSourceActor: { actor: 'actor2' },
generatedLine: 103 originalLine: 103
}; };
bpStore.setActor(location, {}); bpStore.setActor(location, {});
do_check_true(!!bpStore.getActor(location), do_check_true(!!bpStore.getActor(location),
@ -81,9 +81,9 @@ function test_delete_actor() {
// Breakpoint with column // Breakpoint with column
let bpStore = new BreakpointActorMap(); let bpStore = new BreakpointActorMap();
let location = { let location = {
generatedSourceActor: { actor: 'actor1' }, originalSourceActor: { actor: 'actor1' },
generatedLine: 10, originalLine: 10,
generatedColumn: 9 originalColumn: 9
}; };
bpStore.setActor(location, {}); bpStore.setActor(location, {});
bpStore.deleteActor(location); bpStore.deleteActor(location);
@ -92,8 +92,8 @@ function test_delete_actor() {
// Breakpoint without column (whole line breakpoint) // Breakpoint without column (whole line breakpoint)
location = { location = {
generatedSourceActor: { actor: 'actor2' }, originalSourceActor: { actor: 'actor2' },
generatedLine: 103 originalLine: 103
}; };
bpStore.setActor(location, {}); bpStore.setActor(location, {});
bpStore.deleteActor(location); bpStore.deleteActor(location);
@ -103,14 +103,14 @@ function test_delete_actor() {
function test_find_actors() { function test_find_actors() {
let bps = [ let bps = [
{ generatedSourceActor: { actor: "actor1" }, generatedLine: 10 }, { originalSourceActor: { actor: "actor1" }, originalLine: 10 },
{ generatedSourceActor: { actor: "actor1" }, generatedLine: 10, generatedColumn: 3 }, { originalSourceActor: { actor: "actor1" }, originalLine: 10, originalColumn: 3 },
{ generatedSourceActor: { actor: "actor1" }, generatedLine: 10, generatedColumn: 10 }, { originalSourceActor: { actor: "actor1" }, originalLine: 10, originalColumn: 10 },
{ generatedSourceActor: { actor: "actor1" }, generatedLine: 23, generatedColumn: 89 }, { originalSourceActor: { actor: "actor1" }, originalLine: 23, originalColumn: 89 },
{ generatedSourceActor: { actor: "actor2" }, generatedLine: 10, generatedColumn: 1 }, { originalSourceActor: { actor: "actor2" }, originalLine: 10, originalColumn: 1 },
{ generatedSourceActor: { actor: "actor2" }, generatedLine: 20, generatedColumn: 5 }, { originalSourceActor: { actor: "actor2" }, originalLine: 20, originalColumn: 5 },
{ generatedSourceActor: { actor: "actor2" }, generatedLine: 30, generatedColumn: 34 }, { originalSourceActor: { actor: "actor2" }, originalLine: 30, originalColumn: 34 },
{ generatedSourceActor: { actor: "actor2" }, generatedLine: 40, generatedColumn: 56 } { originalSourceActor: { actor: "actor2" }, originalLine: 40, originalColumn: 56 }
]; ];
let bpStore = new BreakpointActorMap(); let bpStore = new BreakpointActorMap();
@ -130,8 +130,8 @@ function test_find_actors() {
// Breakpoints by URL // Breakpoints by URL
bpSet = new Set(bps.filter(bp => { return bp.generatedSourceActor.actorID === "actor1" })); bpSet = new Set(bps.filter(bp => { return bp.originalSourceActor.actorID === "actor1" }));
for (let bp of bpStore.findActors({ generatedSourceActor: { actorID: "actor1" } })) { for (let bp of bpStore.findActors({ originalSourceActor: { actorID: "actor1" } })) {
bpSet.delete(bp); bpSet.delete(bp);
} }
do_check_eq(bpSet.size, 0, do_check_eq(bpSet.size, 0,
@ -139,15 +139,15 @@ function test_find_actors() {
// Breakpoints by URL and line // Breakpoints by URL and line
bpSet = new Set(bps.filter(bp => { return bp.generatedSourceActor.actorID === "actor1" && bp.generatedLine === 10; })); bpSet = new Set(bps.filter(bp => { return bp.originalSourceActor.actorID === "actor1" && bp.originalLine === 10; }));
let first = true; let first = true;
for (let bp of bpStore.findActors({ generatedSourceActor: { actorID: "actor1" }, generatedLine: 10 })) { for (let bp of bpStore.findActors({ originalSourceActor: { actorID: "actor1" }, originalLine: 10 })) {
if (first) { if (first) {
do_check_eq(bp.generatedColumn, undefined, do_check_eq(bp.originalColumn, undefined,
"Should always get the whole line breakpoint first"); "Should always get the whole line breakpoint first");
first = false; first = false;
} else { } else {
do_check_neq(bp.generatedColumn, undefined, do_check_neq(bp.originalColumn, undefined,
"Should not get the whole line breakpoint any time other than first."); "Should not get the whole line breakpoint any time other than first.");
} }
bpSet.delete(bp); bpSet.delete(bp);
@ -161,9 +161,9 @@ function test_duplicate_actors() {
// Breakpoint with column // Breakpoint with column
let location = { let location = {
generatedSourceActor: { actorID: "foo-actor" }, originalSourceActor: { actorID: "foo-actor" },
generatedLine: 10, originalLine: 10,
generatedColumn: 9 originalColumn: 9
}; };
bpStore.setActor(location, {}); bpStore.setActor(location, {});
bpStore.setActor(location, {}); bpStore.setActor(location, {});
@ -172,8 +172,8 @@ function test_duplicate_actors() {
// Breakpoint without column (whole line breakpoint) // Breakpoint without column (whole line breakpoint)
location = { location = {
generatedSourceActor: { actorID: "foo-actor" }, originalSourceActor: { actorID: "foo-actor" },
generatedLine: 15 originalLine: 15
}; };
bpStore.setActor(location, {}); bpStore.setActor(location, {});
bpStore.setActor(location, {}); bpStore.setActor(location, {});