Dbrant has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/214117

Change subject: [WIP] Tabbed browsing.
......................................................................

[WIP] Tabbed browsing.

Bug: T69251
Change-Id: I00967dc2256fc2cb8c251cc5a6a0ebb83e144a6e
---
A icon-svgs/24/ic_add_tab.noflip.svg
A icon-svgs/24/ic_close_gray.noflip.svg
A icon-svgs/24/ic_tab_list.noflip.svg
M wikipedia/AndroidManifest.xml
A wikipedia/res/anim/tab_item_press.xml
A wikipedia/res/anim/tab_list_items_enter.xml
A wikipedia/res/anim/tab_list_items_exit.xml
A wikipedia/res/anim/tab_list_zoom_enter.xml
A wikipedia/res/anim/tab_list_zoom_exit.xml
A wikipedia/res/drawable-hdpi/ic_add_tab.png
A wikipedia/res/drawable-hdpi/ic_close_gray.png
A wikipedia/res/drawable-hdpi/ic_tab_list.png
A wikipedia/res/drawable-ldpi/ic_add_tab.png
A wikipedia/res/drawable-ldpi/ic_close_gray.png
A wikipedia/res/drawable-ldpi/ic_tab_list.png
A wikipedia/res/drawable-mdpi/ic_add_tab.png
A wikipedia/res/drawable-mdpi/ic_close_gray.png
A wikipedia/res/drawable-mdpi/ic_tab_list.png
A wikipedia/res/drawable-xhdpi/ic_add_tab.png
A wikipedia/res/drawable-xhdpi/ic_close_gray.png
A wikipedia/res/drawable-xhdpi/ic_tab_list.png
A wikipedia/res/drawable-xxhdpi/ic_add_tab.png
A wikipedia/res/drawable-xxhdpi/ic_close_gray.png
A wikipedia/res/drawable-xxhdpi/ic_tab_list.png
A wikipedia/res/drawable/tab_item_bottom_gradient.xml
A wikipedia/res/drawable/tab_item_selector.xml
A wikipedia/res/drawable/tab_item_shape.xml
A wikipedia/res/drawable/tab_item_shape_selected.xml
M wikipedia/res/layout/activity_page.xml
M wikipedia/res/layout/dialog_page_info.xml
A wikipedia/res/layout/item_tab_entry.xml
M wikipedia/res/menu/menu_page_actions.xml
A wikipedia/res/menu/menu_page_long_press.xml
A wikipedia/res/menu/menu_tabs.xml
M wikipedia/res/values/strings.xml
M wikipedia/src/main/java/org/wikipedia/page/PageActivity.java
A wikipedia/src/main/java/org/wikipedia/page/PageLongPressHandler.java
M wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java
A wikipedia/src/main/java/org/wikipedia/page/tabs/Tab.java
A wikipedia/src/main/java/org/wikipedia/page/tabs/TabsProvider.java
40 files changed, 1,052 insertions(+), 20 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/android/wikipedia 
refs/changes/17/214117/1

diff --git a/icon-svgs/24/ic_add_tab.noflip.svg 
b/icon-svgs/24/ic_add_tab.noflip.svg
new file mode 100644
index 0000000..0e7f37e
--- /dev/null
+++ b/icon-svgs/24/ic_add_tab.noflip.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="ic_add_tab.noflip.svg">
+  <metadata
+     id="metadata12">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1156"
+     inkscape:window-height="725"
+     id="namedview8"
+     showgrid="false"
+     inkscape:zoom="27.812867"
+     inkscape:cx="6.8946906"
+     inkscape:cy="10.24447"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" />
+  <path
+     d="M0 0h24v24h-24z"
+     fill="none"
+     id="path4" />
+  <path
+     d="M 4,6 2,6 2,20 c 0,1.1 0.9,2 2,2 l 14,0 0,-2 -14,0 z M 20,2 8,2 C 
6.9,2 6,2.9 6,4 l 0,12 c 0,1.1 0.9,2 2,2 l 12,0 c 1.1,0 2,-0.9 2,-2 L 22,4 C 
22,2.9 21.1,2 20,2 Z m -1,9 -4,0 0,4 -2,0 0,-4 -4,0 0,-2 4,0 0,-4 2,0 0,4 4,0 z"
+     id="path6"
+     style="fill:#ffffff"
+     inkscape:connector-curvature="0"
+     sodipodi:nodetypes="ccssccccsssssssssccccccccccccc" />
+</svg>
diff --git a/icon-svgs/24/ic_close_gray.noflip.svg 
b/icon-svgs/24/ic_close_gray.noflip.svg
new file mode 100644
index 0000000..2cbe099
--- /dev/null
+++ b/icon-svgs/24/ic_close_gray.noflip.svg
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="48"
+   height="48"
+   viewBox="0 0 48 48"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="ic_close_gray.noflip.svg">
+  <metadata
+     id="metadata12">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1194"
+     inkscape:window-height="920"
+     id="namedview8"
+     showgrid="false"
+     inkscape:zoom="4.9166667"
+     inkscape:cx="-5.5932198"
+     inkscape:cy="24"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" />
+  <path
+     d="M38 12.83l-2.83-2.83-11.17 11.17-11.17-11.17-2.83 2.83 11.17 
11.17-11.17 11.17 2.83 2.83 11.17-11.17 11.17 11.17 2.83-2.83-11.17-11.17z"
+     id="path4"
+     style="fill:#808080;fill-opacity:1" />
+  <path
+     d="M0 0h48v48h-48z"
+     fill="none"
+     id="path6" />
+</svg>
diff --git a/icon-svgs/24/ic_tab_list.noflip.svg 
b/icon-svgs/24/ic_tab_list.noflip.svg
new file mode 100644
index 0000000..54e0465
--- /dev/null
+++ b/icon-svgs/24/ic_tab_list.noflip.svg
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="ic_tab_list.noflip.svg">
+  <metadata
+     id="metadata12">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1310"
+     inkscape:window-height="880"
+     id="namedview8"
+     showgrid="false"
+<<<<<<< HEAD
+     inkscape:zoom="55.625733"
+     inkscape:cx="3.3728809"
+     inkscape:cy="5.1296812"
+=======
+     inkscape:zoom="29.86"
+     inkscape:cx="11.971591"
+     inkscape:cy="9.8484366"
+>>>>>>> Tabbed browsing prototype.
+     inkscape:window-x="254"
+     inkscape:window-y="57"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" />
+  <path
+<<<<<<< HEAD
+     d="M0 0h24v24h-24z"
+     fill="none"
+     id="path4" />
+  <path
+     d="M 3,5 1,5 1,21 c 0,1.1 0.9,2 2,2 l 16,0 0,-2 -16,0 z M 21,1 7,1 C 
5.9,1 5,1.9 5,3 l 0,14 c 0,1.1 0.9,2 2,2 l 14,0 c 1.1,0 2,-0.9 2,-2 L 23,3 C 
23,1.9 22.1,1 21,1 Z M 21,17 7,17 7,3 21,3 Z"
+     id="path6"
+     style="fill:#ffffff"
+     inkscape:connector-curvature="0"
+     sodipodi:nodetypes="ccssccccsssssssssccccc" />
+=======
+     style="fill:#ffffff"
+     d="m 21.041392,7.0765443 -14.2735549,0 0,9.1439967 c 0,0.872273 
0.7136778,1.58595 1.5859507,1.58595 l 11.1016532,0 c 0.872273,0 
1.585951,-0.713677 1.585951,-1.58595 l 0,-9.1439967 z"
+     id="path4162"
+     inkscape:connector-curvature="0" />
+  <path
+     style="fill:#ffffff"
+     d="m 8.3537878,3.3908514 c -0.8722729,0 -1.5859507,0.7136778 
-1.5859507,1.5859507 l 0,1.090341 14.2735549,0 0,-1.090341 c 0,-0.8722729 
-0.713678,-1.5859507 -1.585951,-1.5859507 l -11.1016532,0 z"
+     id="path4153"
+     inkscape:connector-curvature="0" />
+  <path
+     style="fill:#ffffff"
+     d="m 4.9493418,10.280636 -1.7841944,0 0,9.143997 c 0,0.872273 
0.7136777,1.58595 1.5859507,1.58595 l 11.1016539,0 c 0.872273,0 
1.585951,-0.713677 1.585951,-1.58595 l -12.4893612,0 0,-9.143997 z"
+     id="path4204"
+     inkscape:connector-curvature="0" />
+  <path
+     style="fill:#ffffff"
+     d="m 4.9493418,6.6186236 -0.1982437,0 c -0.872273,0 -1.5859507,0.7136778 
-1.5859507,1.5859506 l 0,1.0903411 1.7841944,0 0,-2.6762917 z"
+     id="path4195"
+     inkscape:connector-curvature="0" />
+>>>>>>> Tabbed browsing prototype.
+</svg>
diff --git a/wikipedia/AndroidManifest.xml b/wikipedia/AndroidManifest.xml
index 21597ad..7447191 100644
--- a/wikipedia/AndroidManifest.xml
+++ b/wikipedia/AndroidManifest.xml
@@ -13,6 +13,7 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" 
/>
+    <uses-permission android:name="android.permission.VIBRATE" />
 
     <!-- For Nearby feature -->
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
diff --git a/wikipedia/res/anim/tab_item_press.xml 
b/wikipedia/res/anim/tab_item_press.xml
new file mode 100644
index 0000000..93a33ec
--- /dev/null
+++ b/wikipedia/res/anim/tab_item_press.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android";>
+    <translate
+        android:duration="200"
+        android:fromYDelta="0"
+        android:toYDelta="-10"
+        android:repeatCount="1"
+        android:repeatMode="reverse"/>
+</set>
\ No newline at end of file
diff --git a/wikipedia/res/anim/tab_list_items_enter.xml 
b/wikipedia/res/anim/tab_list_items_enter.xml
new file mode 100644
index 0000000..45bf0cb
--- /dev/null
+++ b/wikipedia/res/anim/tab_list_items_enter.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android";
+    android:fillAfter="true">
+    <translate
+        android:duration="300"
+        android:fromYDelta="-100%"
+        android:toYDelta="0"/>
+    <scale
+        android:duration="300"
+        android:fromXScale="1.33"
+        android:fromYScale="1.33"
+        android:pivotX="50%"
+        android:pivotY="50%"
+        android:toXScale="1"
+        android:toYScale="1"/>
+</set>
\ No newline at end of file
diff --git a/wikipedia/res/anim/tab_list_items_exit.xml 
b/wikipedia/res/anim/tab_list_items_exit.xml
new file mode 100644
index 0000000..05aed09
--- /dev/null
+++ b/wikipedia/res/anim/tab_list_items_exit.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android";
+    android:fillAfter="true">
+    <translate
+        android:duration="300"
+        android:fromYDelta="0"
+        android:toYDelta="-100%"/>
+    <scale
+        android:duration="300"
+        android:fromXScale="1"
+        android:fromYScale="1"
+        android:pivotX="50%"
+        android:pivotY="50%"
+        android:toXScale="1.33"
+        android:toYScale="1.33"/>
+</set>
\ No newline at end of file
diff --git a/wikipedia/res/anim/tab_list_zoom_enter.xml 
b/wikipedia/res/anim/tab_list_zoom_enter.xml
new file mode 100644
index 0000000..d8b138a
--- /dev/null
+++ b/wikipedia/res/anim/tab_list_zoom_enter.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android";
+    android:fillAfter="true">
+    <translate
+        android:duration="300"
+        android:fromYDelta="0"
+        android:toYDelta="30%"/>
+    <scale
+        android:duration="300"
+        android:fromXScale="1"
+        android:fromYScale="1"
+        android:pivotX="50%"
+        android:pivotY="100%"
+        android:toXScale="0.75"
+        android:toYScale="0.75"/>
+</set>
\ No newline at end of file
diff --git a/wikipedia/res/anim/tab_list_zoom_exit.xml 
b/wikipedia/res/anim/tab_list_zoom_exit.xml
new file mode 100644
index 0000000..08d9446
--- /dev/null
+++ b/wikipedia/res/anim/tab_list_zoom_exit.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android";
+    android:fillAfter="true">
+    <translate
+        android:duration="300"
+        android:fromYDelta="30%"
+        android:toYDelta="0%"/>
+    <scale
+        android:duration="300"
+        android:fromXScale="0.75"
+        android:fromYScale="0.75"
+        android:pivotX="50%"
+        android:pivotY="100%"
+        android:toXScale="1"
+        android:toYScale="1"/>
+</set>
\ No newline at end of file
diff --git a/wikipedia/res/drawable-hdpi/ic_add_tab.png 
b/wikipedia/res/drawable-hdpi/ic_add_tab.png
new file mode 100644
index 0000000..3950fda
--- /dev/null
+++ b/wikipedia/res/drawable-hdpi/ic_add_tab.png
Binary files differ
diff --git a/wikipedia/res/drawable-hdpi/ic_close_gray.png 
b/wikipedia/res/drawable-hdpi/ic_close_gray.png
new file mode 100644
index 0000000..cd33738
--- /dev/null
+++ b/wikipedia/res/drawable-hdpi/ic_close_gray.png
Binary files differ
diff --git a/wikipedia/res/drawable-hdpi/ic_tab_list.png 
b/wikipedia/res/drawable-hdpi/ic_tab_list.png
new file mode 100644
index 0000000..85a763a
--- /dev/null
+++ b/wikipedia/res/drawable-hdpi/ic_tab_list.png
Binary files differ
diff --git a/wikipedia/res/drawable-ldpi/ic_add_tab.png 
b/wikipedia/res/drawable-ldpi/ic_add_tab.png
new file mode 100644
index 0000000..c681198
--- /dev/null
+++ b/wikipedia/res/drawable-ldpi/ic_add_tab.png
Binary files differ
diff --git a/wikipedia/res/drawable-ldpi/ic_close_gray.png 
b/wikipedia/res/drawable-ldpi/ic_close_gray.png
new file mode 100644
index 0000000..a0c4d03
--- /dev/null
+++ b/wikipedia/res/drawable-ldpi/ic_close_gray.png
Binary files differ
diff --git a/wikipedia/res/drawable-ldpi/ic_tab_list.png 
b/wikipedia/res/drawable-ldpi/ic_tab_list.png
new file mode 100644
index 0000000..262730c
--- /dev/null
+++ b/wikipedia/res/drawable-ldpi/ic_tab_list.png
Binary files differ
diff --git a/wikipedia/res/drawable-mdpi/ic_add_tab.png 
b/wikipedia/res/drawable-mdpi/ic_add_tab.png
new file mode 100644
index 0000000..db34796
--- /dev/null
+++ b/wikipedia/res/drawable-mdpi/ic_add_tab.png
Binary files differ
diff --git a/wikipedia/res/drawable-mdpi/ic_close_gray.png 
b/wikipedia/res/drawable-mdpi/ic_close_gray.png
new file mode 100644
index 0000000..79fd948
--- /dev/null
+++ b/wikipedia/res/drawable-mdpi/ic_close_gray.png
Binary files differ
diff --git a/wikipedia/res/drawable-mdpi/ic_tab_list.png 
b/wikipedia/res/drawable-mdpi/ic_tab_list.png
new file mode 100644
index 0000000..003c3b7
--- /dev/null
+++ b/wikipedia/res/drawable-mdpi/ic_tab_list.png
Binary files differ
diff --git a/wikipedia/res/drawable-xhdpi/ic_add_tab.png 
b/wikipedia/res/drawable-xhdpi/ic_add_tab.png
new file mode 100644
index 0000000..f44b86d
--- /dev/null
+++ b/wikipedia/res/drawable-xhdpi/ic_add_tab.png
Binary files differ
diff --git a/wikipedia/res/drawable-xhdpi/ic_close_gray.png 
b/wikipedia/res/drawable-xhdpi/ic_close_gray.png
new file mode 100644
index 0000000..4fb8455
--- /dev/null
+++ b/wikipedia/res/drawable-xhdpi/ic_close_gray.png
Binary files differ
diff --git a/wikipedia/res/drawable-xhdpi/ic_tab_list.png 
b/wikipedia/res/drawable-xhdpi/ic_tab_list.png
new file mode 100644
index 0000000..d00300d
--- /dev/null
+++ b/wikipedia/res/drawable-xhdpi/ic_tab_list.png
Binary files differ
diff --git a/wikipedia/res/drawable-xxhdpi/ic_add_tab.png 
b/wikipedia/res/drawable-xxhdpi/ic_add_tab.png
new file mode 100644
index 0000000..77a2d35
--- /dev/null
+++ b/wikipedia/res/drawable-xxhdpi/ic_add_tab.png
Binary files differ
diff --git a/wikipedia/res/drawable-xxhdpi/ic_close_gray.png 
b/wikipedia/res/drawable-xxhdpi/ic_close_gray.png
new file mode 100644
index 0000000..bf19e3c
--- /dev/null
+++ b/wikipedia/res/drawable-xxhdpi/ic_close_gray.png
Binary files differ
diff --git a/wikipedia/res/drawable-xxhdpi/ic_tab_list.png 
b/wikipedia/res/drawable-xxhdpi/ic_tab_list.png
new file mode 100644
index 0000000..413ea21
--- /dev/null
+++ b/wikipedia/res/drawable-xxhdpi/ic_tab_list.png
Binary files differ
diff --git a/wikipedia/res/drawable/tab_item_bottom_gradient.xml 
b/wikipedia/res/drawable/tab_item_bottom_gradient.xml
new file mode 100644
index 0000000..ebfe12a
--- /dev/null
+++ b/wikipedia/res/drawable/tab_item_bottom_gradient.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android";
+    android:shape="rectangle">
+    <gradient
+        android:startColor="@color/gallery_background"
+        android:endColor="#00000000"
+        android:angle="90"/>
+</shape>
\ No newline at end of file
diff --git a/wikipedia/res/drawable/tab_item_selector.xml 
b/wikipedia/res/drawable/tab_item_selector.xml
new file mode 100644
index 0000000..e1fad34
--- /dev/null
+++ b/wikipedia/res/drawable/tab_item_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android";>
+    <item android:state_checked="true" 
android:drawable="@drawable/tab_item_shape_selected" />
+    <item android:state_pressed="true" 
android:drawable="@drawable/tab_item_shape_selected" />
+    <item android:drawable="@drawable/tab_item_shape" />
+</selector>
\ No newline at end of file
diff --git a/wikipedia/res/drawable/tab_item_shape.xml 
b/wikipedia/res/drawable/tab_item_shape.xml
new file mode 100644
index 0000000..f450050
--- /dev/null
+++ b/wikipedia/res/drawable/tab_item_shape.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android";
+    android:shape="rectangle">
+    <solid android:color="@color/window_background_light"/>
+    <corners android:topLeftRadius="8dp" android:topRightRadius="8dp"/>
+</shape>
\ No newline at end of file
diff --git a/wikipedia/res/drawable/tab_item_shape_selected.xml 
b/wikipedia/res/drawable/tab_item_shape_selected.xml
new file mode 100644
index 0000000..65b7c39
--- /dev/null
+++ b/wikipedia/res/drawable/tab_item_shape_selected.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android";
+    android:shape="rectangle">
+    <solid android:color="@color/gray_highlight"/>
+    <corners android:topLeftRadius="8dp" android:topRightRadius="8dp"/>
+</shape>
\ No newline at end of file
diff --git a/wikipedia/res/layout/activity_page.xml 
b/wikipedia/res/layout/activity_page.xml
index 5f8d4f5..162ff73 100644
--- a/wikipedia/res/layout/activity_page.xml
+++ b/wikipedia/res/layout/activity_page.xml
@@ -9,7 +9,8 @@
 
     <FrameLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
+        android:layout_height="match_parent"
+        android:background="@color/gallery_background">
 
         <!-- The main content view -->
         <FrameLayout
@@ -18,6 +19,23 @@
             android:layout_height="match_parent">
         </FrameLayout>
 
+        <!-- The tabs container -->
+        <FrameLayout
+            android:id="@+id/tabs_container"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone">
+            <ListView
+                android:id="@+id/tabs_list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:divider="@null"
+                android:dividerHeight="0dp"
+                android:stackFromBottom="true"
+                android:transcriptMode="alwaysScroll"
+                />
+        </FrameLayout>
+
         <!-- The search container -->
         <fragment android:layout_width="match_parent" 
android:layout_height="match_parent"
             android:id="@+id/search_fragment"
diff --git a/wikipedia/res/layout/dialog_page_info.xml 
b/wikipedia/res/layout/dialog_page_info.xml
index 577fe9d..caa2a55 100644
--- a/wikipedia/res/layout/dialog_page_info.xml
+++ b/wikipedia/res/layout/dialog_page_info.xml
@@ -69,7 +69,7 @@
             android:padding="8dp"
             android:layout_gravity="center_vertical"
             android:gravity="center"
-            android:src="@drawable/close"
+            android:src="@drawable/ic_close_gray"
             android:background="@drawable/button_selector_transparent"
             android:contentDescription="@string/dialog_close_description" />
     </LinearLayout>
diff --git a/wikipedia/res/layout/item_tab_entry.xml 
b/wikipedia/res/layout/item_tab_entry.xml
new file mode 100644
index 0000000..8998f35
--- /dev/null
+++ b/wikipedia/res/layout/item_tab_entry.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
+    xmlns:tools="http://schemas.android.com/tools";
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:descendantFocusability="blocksDescendants"
+    android:background="@drawable/tab_item_shape">
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            >
+            <ImageView
+                android:id="@+id/tab_item_thumbnail"
+                android:layout_height="32dp"
+                android:layout_width="32dp"
+                android:layout_margin="8dp"
+                android:layout_gravity="center_vertical"
+                android:scaleType="centerCrop"
+                android:src="@drawable/ic_pageimage_placeholder"
+                android:background="@android:color/transparent"
+                android:contentDescription="@null"
+                />
+
+            <TextView
+                android:id="@+id/tab_item_title"
+                android:layout_height="wrap_content"
+                android:layout_width="0dp"
+                android:layout_weight="1"
+                android:layout_gravity="center_vertical"
+                android:maxLines="1"
+                android:ellipsize="end"
+                tools:text="Sample tab title"
+                style="?android:textAppearanceMedium"
+                />
+
+            <ImageView
+                android:id="@+id/tab_item_close"
+                android:layout_height="32dp"
+                android:layout_width="32dp"
+                android:layout_margin="8dp"
+                android:padding="4dp"
+                android:layout_gravity="center_vertical"
+                android:src="@drawable/ic_close_gray"
+                android:contentDescription="@null"
+                android:clickable="true"
+                android:background="@drawable/button_selector_transparent"
+                />
+        </LinearLayout>
+        <View
+            android:id="@+id/tab_item_bottom_gradient"
+            android:layout_width="match_parent"
+            android:layout_height="8dp"
+            android:layout_gravity="bottom"
+            android:background="@drawable/tab_item_bottom_gradient"/>
+    </FrameLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/wikipedia/res/menu/menu_page_actions.xml 
b/wikipedia/res/menu/menu_page_actions.xml
index ba0bda1..6a3177b 100644
--- a/wikipedia/res/menu/menu_page_actions.xml
+++ b/wikipedia/res/menu/menu_page_actions.xml
@@ -3,6 +3,11 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android";
       xmlns:app="http://schemas.android.com/apk/res-auto";>
 
+    <item android:id="@+id/menu_show_tabs"
+        android:title="@string/menu_show_tabs"
+        android:icon="@drawable/ic_tab_list"
+        app:showAsAction="always"
+        />
     <item android:id="@+id/menu_toc"
           android:title="@string/menu_show_toc"
           android:icon="@drawable/ic_toc"
diff --git a/wikipedia/res/menu/menu_page_long_press.xml 
b/wikipedia/res/menu/menu_page_long_press.xml
new file mode 100644
index 0000000..7720162
--- /dev/null
+++ b/wikipedia/res/menu/menu_page_long_press.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android";
+      xmlns:app="http://schemas.android.com/apk/res-auto";
+        >
+    <item android:id="@+id/menu_open_link"
+          android:title="@string/menu_open_link"
+          app:showAsAction="ifRoom" />
+
+    <item android:id="@+id/menu_open_in_new_tab"
+        android:title="@string/menu_open_in_new_tab"
+        app:showAsAction="ifRoom" />
+
+    <item android:id="@+id/menu_save_for_later"
+        android:title="@string/menu_save_page_popup"
+        app:showAsAction="ifRoom" />
+
+</menu>
diff --git a/wikipedia/res/menu/menu_tabs.xml b/wikipedia/res/menu/menu_tabs.xml
new file mode 100644
index 0000000..0571cca
--- /dev/null
+++ b/wikipedia/res/menu/menu_tabs.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android";
+      xmlns:app="http://schemas.android.com/apk/res-auto";
+        >
+    <item android:id="@+id/menu_new_tab"
+          android:icon="@drawable/ic_add_tab"
+          android:title="@string/menu_new_tab"
+          app:showAsAction="ifRoom" />
+
+</menu>
diff --git a/wikipedia/res/values/strings.xml b/wikipedia/res/values/strings.xml
index fa1ec40..a3c3f3d 100644
--- a/wikipedia/res/values/strings.xml
+++ b/wikipedia/res/values/strings.xml
@@ -282,4 +282,9 @@
     <string name="gallery_fair_use_license">Fair use</string>
     <string name="gallery_uploader_unknown">Uploader unknown</string>
     <string name="edit_save_unknown_error">Could not save your edit: 
%s</string>
+    <string name="menu_show_tabs">Show tabs</string>
+    <string name="menu_new_tab">New tab</string>
+    <string name="menu_open_link">Open link</string>
+    <string name="menu_open_in_new_tab">Open in new tab</string>
+    <string name="menu_save_page_popup">Save for later</string>
 </resources>
diff --git a/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java 
b/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java
index 0a94a77..00b12e6 100644
--- a/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java
+++ b/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java
@@ -78,6 +78,15 @@
     private WikipediaApp app;
 
     private View fragmentContainerView;
+    public View getContentView() {
+        return fragmentContainerView;
+    }
+
+    private View tabsContainerView;
+    public View getTabsContainerView() {
+        return tabsContainerView;
+    }
+
     private WikiDrawerLayout drawerLayout;
     private NavDrawerFragment fragmentNavdrawer;
     private SearchArticlesFragment searchFragment;
@@ -88,6 +97,10 @@
 
     private View toolbarContainer;
     private ActionMode currentActionMode;
+    private View toolbarView;
+    public View getToolbarView() {
+        return toolbarView;
+    }
 
     private ActionBarDrawerToggle mDrawerToggle;
     public ActionBarDrawerToggle getDrawerToggle() {
@@ -139,8 +152,8 @@
         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
         setContentView(R.layout.activity_page);
 
-        final Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
-        setSupportActionBar(toolbar);
+        toolbarView = findViewById(R.id.main_toolbar);
+        setSupportActionBar((Toolbar) toolbarView);
         getSupportActionBar().setDisplayHomeAsUpEnabled(true);
 
         toolbarContainer = findViewById(R.id.main_toolbar_container);
@@ -155,6 +168,7 @@
         searchHintText = (TextView) findViewById(R.id.main_search_bar_text);
 
         fragmentContainerView = findViewById(R.id.content_fragment_container);
+        tabsContainerView = findViewById(R.id.tabs_container);
         progressBar = (ProgressBar)findViewById(R.id.main_progressbar);
         progressBar.setMax(PROGRESS_BAR_MAX_VALUE);
         updateProgressBar(false, true, 0);
diff --git 
a/wikipedia/src/main/java/org/wikipedia/page/PageLongPressHandler.java 
b/wikipedia/src/main/java/org/wikipedia/page/PageLongPressHandler.java
new file mode 100644
index 0000000..91ce103
--- /dev/null
+++ b/wikipedia/src/main/java/org/wikipedia/page/PageLongPressHandler.java
@@ -0,0 +1,69 @@
+package org.wikipedia.page;
+
+import org.wikipedia.R;
+import org.wikipedia.history.HistoryEntry;
+import android.content.Context;
+import android.os.Vibrator;
+import android.support.v7.widget.PopupMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+public class PageLongPressHandler {
+    private final PageViewFragmentInternal fragment;
+    private final ViewGroup containerView;
+    private PageActivity activity;
+    private View anchorView;
+
+    private Vibrator vibrator;
+
+    PageLongPressHandler(PageViewFragmentInternal fragment, ViewGroup 
containerView) {
+        this.fragment = fragment;
+        this.containerView = containerView;
+        this.activity = (PageActivity) fragment.getActivity();
+        vibrator = (Vibrator) 
activity.getSystemService(Context.VIBRATOR_SERVICE);
+    }
+
+    public void onLongPress(int x, int y, final PageTitle title, final 
HistoryEntry entry) {
+        // create a temporary view at the location of the click event
+        anchorView = new View(fragment.getActivity());
+        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(1, 1);
+        params.leftMargin = x;
+        params.topMargin = y;
+        anchorView.setLayoutParams(params);
+        containerView.addView(anchorView);
+        // create a popup menu and anchor it to the temporary view
+        PopupMenu popupMenu = new PopupMenu(activity, anchorView);
+        popupMenu.inflate(R.menu.menu_page_long_press);
+        popupMenu.setOnMenuItemClickListener(new 
PopupMenu.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                switch (item.getItemId()) {
+                    case R.id.menu_open_link:
+                        activity.displayNewPage(title, entry);
+                        return true;
+                    case R.id.menu_open_in_new_tab:
+                        fragment.openInNewTab(title, entry);
+                        return true;
+                    default:
+                        break;
+                }
+                return false;
+            }
+        });
+        popupMenu.setOnDismissListener(new PopupMenu.OnDismissListener() {
+            @Override
+            public void onDismiss(PopupMenu menu) {
+                if (anchorView != null) {
+                    containerView.removeView(anchorView);
+                    anchorView = null;
+                }
+            }
+        });
+        popupMenu.show();
+        final int vibrateMillis = 50;
+        vibrator.vibrate(vibrateMillis);
+    }
+
+}
diff --git 
a/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java 
b/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java
index 71367cc..2d8a9e9 100755
--- a/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java
+++ b/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java
@@ -25,6 +25,8 @@
 import org.wikipedia.page.linkpreview.LinkPreviewVersion;
 import org.wikipedia.page.snippet.NoTextSelectedShareAdapter;
 import org.wikipedia.page.snippet.TextSelectedShareAdapter;
+import org.wikipedia.page.tabs.Tab;
+import org.wikipedia.page.tabs.TabsProvider;
 import org.wikipedia.pageimages.PageImage;
 import org.wikipedia.pageimages.PageImagesTask;
 import org.wikipedia.savedpages.ImageUrlMap;
@@ -32,6 +34,7 @@
 import org.wikipedia.savedpages.LoadSavedPageUrlMapTask;
 import org.wikipedia.savedpages.SavePageTask;
 import org.wikipedia.search.SearchBarHideHandler;
+import org.wikipedia.staticdata.MainPageNameData;
 import org.wikipedia.util.ApiUtil;
 import org.wikipedia.util.DimenUtil;
 import org.wikipedia.util.NetworkUtils;
@@ -47,6 +50,7 @@
 import android.app.AlertDialog;
 import android.content.Intent;
 import android.graphics.Bitmap;
+import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
@@ -54,6 +58,7 @@
 import android.support.v4.widget.SwipeRefreshLayout;
 import android.support.v7.app.ActionBarActivity;
 import android.support.v7.view.ActionMode;
+import android.support.v7.widget.PopupMenu;
 import android.text.Html;
 import android.util.Log;
 import android.util.TypedValue;
@@ -64,6 +69,9 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 import javax.net.ssl.SSLException;
@@ -91,11 +99,11 @@
     private int subState = SUBSTATE_NONE;
 
     /**
-     * List of lightweight history items to serve as the backstack for this 
fragment.
+     * List of tabs, each of which contains a backstack of page titles.
      * Since the list consists of Parcelable objects, it can be saved and 
restored from the
      * savedInstanceState of the fragment.
      */
-    private ArrayList<PageBackStackItem> backStack;
+    private ArrayList<Tab> tabList;
 
     /**
      * Whether to save the full page content as soon as it's loaded.
@@ -143,6 +151,7 @@
     private ReferenceDialog referenceDialog;
     private EditHandler editHandler;
     private ActionMode findInPageActionMode;
+    private PageLongPressHandler longPressHandler;
 
     private WikipediaApp app;
 
@@ -157,6 +166,8 @@
 
     private TextSelectedShareAdapter textSelectedShareAdapter;
     private NoTextSelectedShareAdapter noTextSelectedShareAdapter;
+
+    private TabsProvider tabsProvider;
 
     public ObservableWebView getWebView() {
         return webView;
@@ -175,7 +186,7 @@
     }
 
     public PageViewFragmentInternal() {
-        backStack = new ArrayList<>();
+        tabList = new ArrayList<>();
     }
 
     private void displayLeadSection() {
@@ -227,6 +238,12 @@
 
         refreshView.setRefreshing(false);
         ((PageActivity) getActivity()).updateProgressBar(true, true, 0);
+
+        // update tab display, in case it's showing
+        
getCurrentTab().getBackStack().get(getCurrentTab().getBackStack().size() - 
1).getTitle().setThumbUrl(title.getThumbUrl());
+        
getCurrentTab().getBackStack().get(getCurrentTab().getBackStack().size() - 
1).getTitle().setDescription(
+                title.getDescription());
+        tabsProvider.invalidate();
     }
 
     private void displayNonLeadSection(int index) {
@@ -317,7 +334,10 @@
         connectionIssueFunnel = new ConnectionIssueFunnel(app);
 
         if (savedInstanceState != null) {
-            backStack = savedInstanceState.getParcelableArrayList("backStack");
+            tabList = savedInstanceState.getParcelableArrayList("tabList");
+        } else if (tabList.size() == 0) {
+            // fresh launch, so initialize with a single tab
+            tabList.add(new Tab());
         }
 
         updateFontSize();
@@ -427,10 +447,64 @@
 
         pageSequenceNum = 0;
 
+        tabsProvider = new TabsProvider((PageActivity) getActivity(), tabList);
+        tabsProvider.setTabsProviderListener(new 
TabsProvider.TabsProviderListener() {
+            @Override
+            public void onCancelTabView() {
+                tabsProvider.exitTabMode();
+            }
+
+            @Override
+            public void onTabSelected(int position) {
+                // move the selected tab to the bottom of the list, and 
navigate to it!
+                // (but only if it's a different tab than the one currently in 
view!
+                if (position != tabList.size() - 1) {
+                    Tab tab = tabList.remove(position);
+                    tabList.add(tab);
+                    loadPageFromBackStack();
+                }
+                tabsProvider.exitTabMode();
+            }
+
+            @Override
+            public void onNewTabRequested() {
+                // just load the main page into a new tab...
+                PageTitle newTitle = new 
PageTitle(MainPageNameData.valueFor(app.getLanguage()), app.getPrimarySite());
+                HistoryEntry newEntry = new HistoryEntry(newTitle, 
HistoryEntry.SOURCE_INTERNAL_LINK);
+                openInNewTab(newTitle, newEntry);
+            }
+
+            @Override
+            public void onCloseTabRequested(int position) {
+                tabList.remove(position);
+                if (position < tabList.size()) {
+                    // if it's not the topmost tab, then just delete it and 
update the tab list...
+                    tabsProvider.invalidate();
+                } else if (tabList.size() > 0) {
+                    // but if it's the topmost tab, then load the topmost page 
in the next tab.
+                    loadPageFromBackStack();
+                } else {
+                    // and if the last tab was closed, then finish the 
activity!
+                    getActivity().finish();
+                }
+            }
+        });
+
+        longPressHandler = new PageLongPressHandler(this, (ViewGroup) 
((PageActivity) getActivity()).getContentView());
+        webView.addOnLongPressListener(new 
ObservableWebView.OnLongPressListener() {
+            @Override
+            public boolean onLongPress(float x, float y, final String 
linkTitle) {
+                PageTitle newTitle = 
titleOriginal.getSite().titleForInternalLink(linkTitle);
+                HistoryEntry newEntry = new HistoryEntry(newTitle, 
HistoryEntry.SOURCE_INTERNAL_LINK);
+                longPressHandler.onLongPress((int) x, (int) y, newTitle, 
newEntry);
+                return true;
+            }
+        });
+
         // if we already have pages in the backstack (whether it's from 
savedInstanceState, or
         // from being stored in the activity's fragment backstack), then load 
the topmost page
         // on the backstack.
-        if (backStack.size() > 0) {
+        if (getCurrentTab().getBackStack().size() > 0) {
             loadPageFromBackStack();
         }
     }
@@ -460,7 +534,7 @@
         super.onSaveInstanceState(outState);
         // update the topmost entry in the backstack
         updateBackStackItem();
-        outState.putParcelableArrayList("backStack", backStack);
+        outState.putParcelableArrayList("tabList", tabList);
     }
 
     @Override
@@ -469,15 +543,19 @@
         ((ActionBarActivity) getActivity()).getSupportActionBar().setTitle("");
     }
 
+    public Tab getCurrentTab() {
+        return tabList.get(tabList.size() - 1);
+    }
+
     /**
      * Pop the topmost entry from the backstack.
      * Does NOT automatically load the next topmost page on the backstack.
      */
     private void popBackStack() {
-        if (backStack.size() == 0) {
+        if (getCurrentTab().getBackStack().size() == 0) {
             return;
         }
-        backStack.remove(backStack.size() - 1);
+        
getCurrentTab().getBackStack().remove(getCurrentTab().getBackStack().size() - 
1);
     }
 
     /**
@@ -485,7 +563,7 @@
      */
     private void pushBackStack() {
         PageBackStackItem item = new PageBackStackItem(titleOriginal, 
curEntry);
-        backStack.add(item);
+        getCurrentTab().getBackStack().add(item);
     }
 
     /**
@@ -494,21 +572,37 @@
      * Should be done right before loading a new page.
      */
     private void updateBackStackItem() {
-        if (backStack.size() == 0) {
+        if (getCurrentTab().getBackStack().size() == 0) {
             return;
         }
-        PageBackStackItem item = backStack.get(backStack.size() - 1);
+        PageBackStackItem item = getCurrentTab().getBackStack().get(
+                getCurrentTab().getBackStack().size() - 1);
         item.setScrollY(webView.getScrollY());
     }
 
     private void loadPageFromBackStack() {
-        if (backStack.size() == 0) {
+        if (getCurrentTab().getBackStack().size() == 0) {
             return;
         }
-        PageBackStackItem item = backStack.get(backStack.size() - 1);
+        PageBackStackItem item = getCurrentTab().getBackStack().get(
+                getCurrentTab().getBackStack().size() - 1);
         // display the page based on the backstack item, stage the scrollY 
position based on
         // the backstack item.
         displayNewPage(item.getTitle(), item.getHistoryEntry(), true, false, 
item.getScrollY());
+    }
+
+    public void openInNewTab(PageTitle title, HistoryEntry entry) {
+        // create a new tab
+        Tab tab = new Tab();
+        // make this tab current
+        tabList.add(tab);
+        // clear out our current title
+        this.titleOriginal = null;
+        this.title = null;
+        this.curEntry = null;
+        // and... that should be it.
+        ((PageActivity) getActivity()).displayNewPage(title, entry);
+        tabsProvider.showAndHideTabs();
     }
 
     /**
@@ -561,6 +655,8 @@
         if (pushBackStack) {
             pushBackStack();
         }
+
+        tabsProvider.invalidate();
 
         // increment our sequence number, so that any async tasks that depend 
on the sequence
         // will invalidate themselves upon completion.
@@ -650,7 +746,7 @@
     }
 
     private boolean isFirstPage() {
-        return backStack.size() <= 1 && !webView.canGoBack();
+        return getCurrentTab().getBackStack().size() <= 1 && 
!webView.canGoBack();
     }
 
     public Bitmap getLeadImageBitmap() {
@@ -954,13 +1050,17 @@
                 langIntent.setClass(getActivity(), LangLinksActivity.class);
                 
langIntent.setAction(LangLinksActivity.ACTION_LANGLINKS_FOR_TITLE);
                 langIntent.putExtra(LangLinksActivity.EXTRA_PAGETITLE, title);
-                getActivity().startActivityForResult(langIntent, 
PageActivity.ACTIVITY_REQUEST_LANGLINKS);
+                getActivity().startActivityForResult(langIntent,
+                                                     
PageActivity.ACTIVITY_REQUEST_LANGLINKS);
                 return true;
             case R.id.menu_find_in_page:
                 showFindInPage();
                 return true;
             case R.id.menu_themechooser:
                 ((PageActivity) getActivity()).showThemeChooser();
+                return true;
+            case R.id.menu_show_tabs:
+                tabsProvider.enterTabMode();
                 return true;
             default:
                 return super.onOptionsItemSelected(item);
@@ -1098,6 +1198,9 @@
                 if (mobileView.has("description")) {
                     
title.setDescription(Utils.capitalizeFirstChar(mobileView.getString("description")));
                 }
+                // update the title and description in the original title...
+                titleOriginal.setThumbUrl(title.getThumbUrl());
+                titleOriginal.setDescription(title.getDescription());
             }
             return super.processResult(result);
         }
@@ -1434,10 +1537,20 @@
         if (closeFindInPage()) {
             return true;
         }
-        if (backStack.size() > 1) {
+        if (tabsProvider.onBackPressed()) {
+            return true;
+        }
+        if (getCurrentTab().getBackStack().size() > 1) {
             popBackStack();
             loadPageFromBackStack();
             return true;
+        } else if (tabList.size() > 1) {
+            // if we're at the end of the current tab's backstack, and we have 
additional
+            // tabs available, then pop the current tab, and load the topmost 
page in that tab.
+            tabList.remove(tabList.size() - 1);
+            loadPageFromBackStack();
+            tabsProvider.enterTabMode();
+            return true;
         }
         return false;
     }
diff --git a/wikipedia/src/main/java/org/wikipedia/page/tabs/Tab.java 
b/wikipedia/src/main/java/org/wikipedia/page/tabs/Tab.java
new file mode 100644
index 0000000..451f576
--- /dev/null
+++ b/wikipedia/src/main/java/org/wikipedia/page/tabs/Tab.java
@@ -0,0 +1,43 @@
+package org.wikipedia.page.tabs;
+
+import org.wikipedia.page.PageBackStackItem;
+import android.os.Parcel;
+import android.os.Parcelable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Tab implements Parcelable {
+    private final List<PageBackStackItem> backStack;
+    public List<PageBackStackItem> getBackStack() {
+        return backStack;
+    }
+
+    public Tab() {
+        backStack = new ArrayList<>();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeList(backStack);
+    }
+
+    private Tab(Parcel in) {
+        backStack = (ArrayList<PageBackStackItem>) 
in.readArrayList(Tab.class.getClassLoader());
+    }
+
+    public static final Parcelable.Creator<Tab> CREATOR
+            = new Parcelable.Creator<Tab>() {
+        public Tab createFromParcel(Parcel in) {
+            return new Tab(in);
+        }
+
+        public Tab[] newArray(int size) {
+            return new Tab[size];
+        }
+    };
+}
diff --git a/wikipedia/src/main/java/org/wikipedia/page/tabs/TabsProvider.java 
b/wikipedia/src/main/java/org/wikipedia/page/tabs/TabsProvider.java
new file mode 100644
index 0000000..867fbad
--- /dev/null
+++ b/wikipedia/src/main/java/org/wikipedia/page/tabs/TabsProvider.java
@@ -0,0 +1,366 @@
+package org.wikipedia.page.tabs;
+
+import org.wikipedia.R;
+import org.wikipedia.Utils;
+import org.wikipedia.WikipediaApp;
+import org.wikipedia.page.PageActivity;
+import org.wikipedia.page.PageBackStackItem;
+import org.wikipedia.page.PageTitle;
+
+import com.squareup.picasso.Picasso;
+import android.support.v7.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import java.util.List;
+
+public class TabsProvider {
+
+    private PageActivity parentActivity;
+    private float displayDensity;
+
+    private View pageContentView;
+    private View tabContainerView;
+    private ListView tabListView;
+    private TabListAdapter tabListAdapter;
+
+    private ActionMode tabActionMode;
+
+    List<Tab> tabList;
+
+    public interface TabsProviderListener {
+        void onCancelTabView();
+        void onTabSelected(int position);
+        void onNewTabRequested();
+        void onCloseTabRequested(int position);
+    }
+    private TabsProviderListener providerListener;
+    public void setTabsProviderListener(TabsProviderListener listener) {
+        providerListener = listener;
+    }
+
+    public TabsProvider(PageActivity parentActivity, List<Tab> tabList) {
+        this.parentActivity = parentActivity;
+        this.tabList = tabList;
+        displayDensity = 
parentActivity.getResources().getDisplayMetrics().density;
+
+        pageContentView = parentActivity.getContentView();
+        tabContainerView = parentActivity.getTabsContainerView();
+        tabListView = (ListView) tabContainerView.findViewById(R.id.tabs_list);
+        tabListAdapter = new 
TabListAdapter(parentActivity.getLayoutInflater());
+        tabListView.setAdapter(tabListAdapter);
+
+        tabContainerView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (providerListener != null) {
+                    providerListener.onCancelTabView();
+                }
+            }
+        });
+
+        tabListView.setOnItemClickListener(new 
AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int 
position, long id) {
+                if (providerListener != null) {
+                    providerListener.onTabSelected(position);
+                }
+            }
+        });
+
+    }
+
+    public boolean onBackPressed() {
+        if (tabActionMode != null) {
+            exitTabMode();
+            return true;
+        }
+        return false;
+    }
+
+    public void enterTabMode() {
+        enterTabMode(null);
+    }
+
+    private void enterTabMode(final Runnable onTabModeEntered) {
+        if (tabActionMode != null) {
+            // already inside action mode...
+            // but make sure to update the list of tabs.
+            tabListAdapter.notifyDataSetInvalidated();
+            tabListView.smoothScrollToPosition(tabList.size() - 1);
+            onTabModeEntered.run();
+            return;
+        }
+        parentActivity.startSupportActionMode(new ActionMode.Callback() {
+            private final String actionModeTag = "actionModeTabList";
+            @Override
+            public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+                tabActionMode = mode;
+                mode.getMenuInflater().inflate(R.menu.menu_tabs, menu);
+                Animation anim = AnimationUtils.loadAnimation(parentActivity,
+                                                              
R.anim.tab_list_zoom_enter);
+                parentActivity.getContentView().startAnimation(anim);
+                layoutTabList(onTabModeEntered);
+                return true;
+            }
+
+            @Override
+            public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+                mode.setTag(actionModeTag);
+                // find the action mode base view, and give it an empty click 
listener
+                View v = parentActivity.findViewById(R.id.action_mode_bar);
+                v.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                    }
+                });
+                return false;
+            }
+
+            @Override
+            public boolean onActionItemClicked(ActionMode mode, MenuItem item) 
{
+                switch (item.getItemId()) {
+                    case R.id.menu_new_tab:
+                        if (providerListener != null) {
+                            providerListener.onNewTabRequested();
+                        }
+                        return true;
+                    default:
+                        break;
+                }
+                return false;
+            }
+
+            @Override
+            public void onDestroyActionMode(ActionMode mode) {
+                Animation anim = AnimationUtils.loadAnimation(parentActivity, 
R.anim.tab_list_zoom_exit);
+                parentActivity.getContentView().startAnimation(anim);
+                hideTabList();
+                tabActionMode = null;
+                parentActivity.showToolbar();
+            }
+        });
+    }
+
+    public void exitTabMode() {
+        if (tabActionMode != null) {
+            tabActionMode.finish();
+        }
+    }
+
+    public void showAndHideTabs() {
+        enterTabMode(new Runnable() {
+            final int animDelay = 500;
+            @Override
+            public void run() {
+                tabContainerView.postDelayed(new Runnable() {
+                    @Override
+                    public void run() {
+                        exitTabMode();
+                    }
+                }, animDelay);
+            }
+        });
+    }
+
+    private void layoutTabList(final Runnable onTabModeEntered) {
+        tabContainerView.setVisibility(View.VISIBLE);
+        tabListAdapter.notifyDataSetInvalidated();
+
+        // size the listview to be the same width as the scaled-down webview...
+        final float proportionHorz = 0.25f;
+        final float proportionVert = 0.3f;
+        final int heightOffset = 16;
+        int toolbarHeight = Utils.getActionBarSize(parentActivity);
+        int maxHeight = (int) (pageContentView.getHeight() * proportionVert + 
pageContentView.getHeight() * proportionHorz - toolbarHeight - heightOffset * 
displayDensity);
+        FrameLayout.LayoutParams params = new 
FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, maxHeight);
+        float margin = pageContentView.getWidth() * proportionHorz / 2f;
+        params.leftMargin = (int) margin;
+        params.rightMargin = (int) margin;
+        params.topMargin = toolbarHeight;
+        tabListView.setLayoutParams(params);
+
+        Animation anim = AnimationUtils.loadAnimation(parentActivity,
+                                                      
R.anim.tab_list_items_enter);
+        anim.setAnimationListener(new Animation.AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) {
+
+            }
+
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                if (onTabModeEntered != null) {
+                    onTabModeEntered.run();
+                }
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) {
+
+            }
+        });
+        tabListView.startAnimation(anim);
+        // scroll to the bottom of the tab list
+        tabListView.smoothScrollToPosition(tabList.size() - 1);
+    }
+
+    private void hideTabList() {
+        Animation anim = AnimationUtils.loadAnimation(parentActivity,
+                                                      
R.anim.tab_list_items_exit);
+        anim.setAnimationListener(new Animation.AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) {
+
+            }
+
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                tabContainerView.setVisibility(View.GONE);
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) {
+
+            }
+        });
+        tabListView.startAnimation(anim);
+    }
+
+    public void invalidate() {
+        tabListAdapter.notifyDataSetInvalidated();
+    }
+
+
+    private View.OnTouchListener onItemTouchListener = new 
View.OnTouchListener() {
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    Animation anim = 
AnimationUtils.loadAnimation(parentActivity, R.anim.tab_item_press);
+                    v.startAnimation(anim);
+                    break;
+                default:
+                    break;
+            }
+            return false;
+        }
+    };
+
+    private View.OnClickListener onItemCloseButtonListener = new 
View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            ViewHolder holder = (ViewHolder) v.getTag();
+            final int position = holder.position;
+            Animation anim = AnimationUtils.loadAnimation(parentActivity, 
R.anim.slide_out_right);
+            anim.setAnimationListener(new Animation.AnimationListener() {
+                @Override
+                public void onAnimationStart(Animation animation) {
+
+                }
+
+                @Override
+                public void onAnimationEnd(Animation animation) {
+                    if (providerListener != null) {
+                        providerListener.onCloseTabRequested(position);
+                    }
+                }
+
+                @Override
+                public void onAnimationRepeat(Animation animation) {
+
+                }
+            });
+            holder.container.startAnimation(anim);
+        }
+    };
+
+    private class ViewHolder {
+        private View container;
+        private TextView title;
+        private ImageView thumbnail;
+        private View gradient;
+        private View closeButton;
+        int position;
+    }
+
+    private final class TabListAdapter extends BaseAdapter {
+        private final LayoutInflater inflater;
+
+        private TabListAdapter(LayoutInflater inflater) {
+            this.inflater = inflater;
+        }
+
+        @Override
+        public int getCount() {
+            return tabList.size();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return tabList.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            ViewHolder viewHolder;
+            if (convertView == null) {
+                viewHolder = new ViewHolder();
+                convertView = inflater.inflate(R.layout.item_tab_entry, 
parent, false);
+                convertView.setTag(viewHolder);
+                viewHolder.container = convertView;
+                viewHolder.title = (TextView) 
convertView.findViewById(R.id.tab_item_title);
+                viewHolder.thumbnail = (ImageView) 
convertView.findViewById(R.id.tab_item_thumbnail);
+                viewHolder.gradient = 
convertView.findViewById(R.id.tab_item_bottom_gradient);
+                viewHolder.closeButton = 
convertView.findViewById(R.id.tab_item_close);
+            } else {
+                viewHolder = (ViewHolder) convertView.getTag();
+            }
+            viewHolder.position = position;
+            viewHolder.container.setOnTouchListener(
+                    position == tabList.size() - 1 ? null : 
onItemTouchListener);
+            viewHolder.closeButton.setTag(viewHolder);
+            
viewHolder.closeButton.setOnClickListener(onItemCloseButtonListener);
+            // hide the shadow if this is the topmost tab
+            viewHolder.gradient.setVisibility(
+                    position == tabList.size() - 1 ? View.GONE : View.VISIBLE);
+
+            List<PageBackStackItem> backstack = 
tabList.get(position).getBackStack();
+            if (backstack.size() > 0) {
+                PageTitle title = backstack.get(backstack.size() - 
1).getTitle();
+                viewHolder.title.setText(title.getDisplayText());
+                String thumbnail = title.getThumbUrl();
+                if (WikipediaApp.getInstance().showImages() && thumbnail != 
null) {
+                    Picasso.with(parentActivity)
+                           .load(thumbnail)
+                           .placeholder(R.drawable.ic_pageimage_placeholder)
+                           .error(R.drawable.ic_pageimage_placeholder)
+                           .into(viewHolder.thumbnail);
+                } else {
+                    Picasso.with(parentActivity)
+                           .load(R.drawable.ic_pageimage_placeholder)
+                           .into(viewHolder.thumbnail);
+                }
+            }
+            return convertView;
+        }
+
+    }
+
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/214117
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I00967dc2256fc2cb8c251cc5a6a0ebb83e144a6e
Gerrit-PatchSet: 1
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Dbrant <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to