Yuvipanda has uploaded a new change for review.

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


Change subject: Media detail page redone as a "slide-up" panel.
......................................................................

Media detail page redone as a "slide-up" panel.

Loads and displays default or English description, and categories.
No caching of this info yet.

Scrollable pane is a ListView, with the title/desc/category label
in a 'header' view along with a spacer view. The height of the spacer
is set dynamically to the height of the total fragment minus 48dp,
giving room for an initially-visible title section and a little
spillover so you can see it's scrollable.

Clicking on a category in the cats list opens the category page in
an external web browser. In the future this should open the category
within the app, but we don't have a per-cat view yet.

Description and category list are not yet editable.

Change-Id: I46d0a77481dbe64a268a72f3efe49ae72168541f
---
A commons/res/drawable/media_info_shadow.xml
A commons/res/layout/detail_category_item.xml
A commons/res/layout/detail_main_panel.xml
M commons/res/layout/fragment_media_detail.xml
M commons/res/values-qq/strings.xml
M commons/res/values/strings.xml
M commons/src/main/java/org/wikimedia/commons/CommonsApplication.java
M commons/src/main/java/org/wikimedia/commons/Media.java
A commons/src/main/java/org/wikimedia/commons/MediaDataExtractor.java
M commons/src/main/java/org/wikimedia/commons/Utils.java
M commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
M commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java
A commons/src/main/java/org/wikimedia/commons/media/MediaDetailSpacer.java
13 files changed, 586 insertions(+), 30 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/android/commons 
refs/changes/39/81139/1

diff --git a/commons/res/drawable/media_info_shadow.xml 
b/commons/res/drawable/media_info_shadow.xml
new file mode 100644
index 0000000..576f0d2
--- /dev/null
+++ b/commons/res/drawable/media_info_shadow.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android";>
+    <gradient
+            android:startColor="#00000000"
+            android:endColor="#ff000000"
+            android:angle="270"
+            >
+    </gradient>
+</shape>
\ No newline at end of file
diff --git a/commons/res/layout/detail_category_item.xml 
b/commons/res/layout/detail_category_item.xml
new file mode 100644
index 0000000..385d13d
--- /dev/null
+++ b/commons/res/layout/detail_category_item.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android";
+          android:layout_width="match_parent"
+          android:layout_height="wrap_content"
+          android:minHeight="48dp"
+          android:padding="8dp"
+          android:gravity="center_vertical"
+          android:id="@+id/mediaDetailCategoryItemText"
+          android:textSize="18sp"
+          android:background="#AA000000"
+          />
diff --git a/commons/res/layout/detail_main_panel.xml 
b/commons/res/layout/detail_main_panel.xml
new file mode 100644
index 0000000..dd6f9ae
--- /dev/null
+++ b/commons/res/layout/detail_main_panel.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
+              android:orientation="vertical"
+              android:layout_width="fill_parent"
+              android:layout_height="fill_parent">
+
+    <!-- Placeholder. Height gets set at runtime based on container size; the 
initial value is a hack to keep
+         the detail info offscreen until it's placed properly. May be a better 
way to do this. -->
+    <org.wikimedia.commons.media.MediaDetailSpacer
+            android:layout_width="fill_parent"
+            android:layout_height="1600dp"
+            android:id="@+id/mediaDetailSpacer"/>
+    <LinearLayout
+            android:orientation="vertical"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:background="#AA000000"
+            android:padding="8dp">
+        <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Title of the media"
+                android:id="@+id/mediaDetailTitle"
+                android:layout_gravity="left|start"
+                android:textColor="@android:color/white"
+                android:textSize="18sp" /> <!-- 18sp == MediumText -->
+        <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Description of the media goes here. This can 
potentially be fairly long, and will need to wrap across multiple lines. We 
hope it looks nice though."
+                android:id="@+id/mediaDetailDesc"
+                android:layout_gravity="left|start"/>
+        <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/detail_panel_cats_label"
+                android:textSize="18sp"
+                android:layout_gravity="left|start"
+                android:paddingTop="24dp" 
android:textColor="@android:color/white"/>
+        <LinearLayout
+                android:orientation="vertical"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:id="@+id/mediaDetailCategoryList"
+                android:layout_gravity="left|start"/>
+    </LinearLayout>
+</LinearLayout>
diff --git a/commons/res/layout/fragment_media_detail.xml 
b/commons/res/layout/fragment_media_detail.xml
index 6a4bdbd..b1d566a 100644
--- a/commons/res/layout/fragment_media_detail.xml
+++ b/commons/res/layout/fragment_media_detail.xml
@@ -28,31 +28,14 @@
                android:scaleType="fitCenter"
             />
 
-    <RelativeLayout
+    <ListView
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
-            android:layout_gravity="center|bottom"
-            android:background="#AA000000"
-            android:padding="8dp"
-            >
-
-        <EditText
-                android:id="@+id/mediaDetailTitle"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:imeOptions="flagNoExtractUi"
-                android:inputType="textNoSuggestions"
-                android:singleLine="true"
-                android:textColor="#FFFFFF"/>
-       <!-- <TextView
-                android:id="@+id/mediaDetailDescription"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_below="@id/mediaDetailTitle"
-                android:layout_alignParentBottom="true"
-                style="?android:textAppearanceSmall"
-                android:textColor="#FFFFFFFF"
-                /> -->
-    </RelativeLayout>
+            android:id="@+id/mediaDetailListView"
+            android:divider="#00A0A0A0"
+            android:fillViewport="true"
+            android:background="@android:color/transparent"
+            android:cacheColorHint="@android:color/transparent"
+            />
 
 </FrameLayout>
\ No newline at end of file
diff --git a/commons/res/values-qq/strings.xml 
b/commons/res/values-qq/strings.xml
index 956329b..e11b059 100644
--- a/commons/res/values-qq/strings.xml
+++ b/commons/res/values-qq/strings.xml
@@ -89,4 +89,7 @@
   <string name="welcome_final_text">Message asking user if they understand 
what kinds of images to upload.</string>
   <string name="welcome_final_button_text">Button text for confirming the user 
understands what kinds of images to upload.
 {{Identical|Yes}}</string>
+    <string name="detail_panel_cats_label">Label for categories list in media 
detail panel</string>
+    <string name="detail_panel_cats_loading">Placeholder for categories list 
in media detail panel, while loading from network.</string>
+    <string name="detail_panel_cats_none">Placeholder for categories list in 
media detail panel, if no categories found.</string>
 </resources>
diff --git a/commons/res/values/strings.xml b/commons/res/values/strings.xml
index f3d4ac7..7bff4bb 100644
--- a/commons/res/values/strings.xml
+++ b/commons/res/values/strings.xml
@@ -102,4 +102,7 @@
     <string name="welcome_copyright_subtext">Avoid copyrighted materials you 
found from the Internet as well as images of posters, book covers, etc.</string>
     <string name="welcome_final_text">You think you got it?</string>
     <string name="welcome_final_button_text">Yes!</string>
+    <string name="detail_panel_cats_label">Categories</string>
+    <string name="detail_panel_cats_loading">Loading...</string>
+    <string name="detail_panel_cats_none">None selected</string>
 </resources>
diff --git 
a/commons/src/main/java/org/wikimedia/commons/CommonsApplication.java 
b/commons/src/main/java/org/wikimedia/commons/CommonsApplication.java
index d41af87..ecf5097 100644
--- a/commons/src/main/java/org/wikimedia/commons/CommonsApplication.java
+++ b/commons/src/main/java/org/wikimedia/commons/CommonsApplication.java
@@ -210,5 +210,4 @@
         return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
                 pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
     }
-
 }
diff --git a/commons/src/main/java/org/wikimedia/commons/Media.java 
b/commons/src/main/java/org/wikimedia/commons/Media.java
index d300f30..2d33c76 100644
--- a/commons/src/main/java/org/wikimedia/commons/Media.java
+++ b/commons/src/main/java/org/wikimedia/commons/Media.java
@@ -2,6 +2,7 @@
 
 import android.net.Uri;
 import android.os.*;
+import android.util.Log;
 
 import java.util.*;
 import java.util.regex.*;
@@ -19,6 +20,8 @@
     };
 
     protected Media() {
+        this.categories = new ArrayList<String>();
+        this.descriptions = new HashMap<String, String>();
     }
 
     private HashMap<String, Object> tags = new HashMap<String, Object>();
@@ -127,10 +130,11 @@
         this.license = license;
     }
 
+    // Primary metadata fields
     protected Uri localUri;
     protected String imageUrl;
     protected String filename;
-    protected String description;
+    protected String description; // monolingual description on input...
     protected long dataLength;
     protected Date dateCreated;
     protected Date dateUploaded;
@@ -141,8 +145,45 @@
 
     protected String creator;
 
+    protected ArrayList<String> categories; // as loaded at runtime?
+    protected Map<String, String> descriptions; // multilingual descriptions 
as loaded
+
+    public ArrayList<String> getCategories() {
+        return (ArrayList<String>)categories.clone(); // feels dirty
+    }
+
+    public void setCategories(List<String> categories) {
+        this.categories.removeAll(this.categories);
+        this.categories.addAll(categories);
+    }
+
+    public void setDescriptions(Map<String,String> descriptions) {
+        for (String key : this.descriptions.keySet()) {
+            this.descriptions.remove(key);
+        }
+        for (String key : descriptions.keySet()) {
+            this.descriptions.put(key, descriptions.get(key));
+        }
+    }
+
+    public String getDescription(String preferredLanguage) {
+        if (descriptions.containsKey(preferredLanguage)) {
+            // See if the requested language is there.
+            return descriptions.get(preferredLanguage);
+        } else if (descriptions.containsKey("en")) {
+            // Ah, English. Language of the world, until the Chinese crush us.
+            return descriptions.get("en");
+        } else if (descriptions.containsKey("default")) {
+            // No languages marked...
+            return descriptions.get("default");
+        } else {
+            // FIXME: return the first available non-English description?
+            return "";
+        }
+    }
 
     public Media(Uri localUri, String imageUrl, String filename, String 
description, long dataLength, Date dateCreated, Date dateUploaded, String 
creator) {
+        this();
         this.localUri = localUri;
         this.imageUrl = imageUrl;
         this.filename = filename;
@@ -170,6 +211,8 @@
         parcel.writeInt(width);
         parcel.writeInt(height);
         parcel.writeString(license);
+        parcel.writeStringList(categories);
+        parcel.writeMap(descriptions);
     }
 
     public Media(Parcel in) {
@@ -185,6 +228,8 @@
         width = in.readInt();
         height = in.readInt();
         license = in.readString();
+        in.readStringList(categories);
+        descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
     }
 
     public void setDescription(String description) {
diff --git 
a/commons/src/main/java/org/wikimedia/commons/MediaDataExtractor.java 
b/commons/src/main/java/org/wikimedia/commons/MediaDataExtractor.java
new file mode 100644
index 0000000..04957b5
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/MediaDataExtractor.java
@@ -0,0 +1,252 @@
+package org.wikimedia.commons;
+
+import android.util.Log;
+import org.mediawiki.api.ApiResult;
+import org.mediawiki.api.MWApi;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Fetch additional media data from the network that we don't store locally.
+ *
+ * This includes things like category lists and multilingual descriptions,
+ * which are not intrinsic to the media and may change due to editing.
+ */
+public class MediaDataExtractor {
+    private boolean fetched;
+    private boolean processed;
+
+    private String filename;
+    private ArrayList<String> categories;
+    private Map<String, String> descriptions;
+    private String author;
+    private Date date;
+
+    /**
+     * @param filename of the target media object, should include 'File:' 
prefix
+     */
+    public MediaDataExtractor(String filename) {
+        this.filename = filename;
+        categories = new ArrayList<String>();
+        descriptions = new HashMap<String, String>();
+        fetched = false;
+        processed = false;
+    }
+
+    /**
+     * Actually fetch the data over the network.
+     * todo: use local caching?
+     *
+     * Warning: synchronous i/o, call on a background thread
+     */
+    public void fetch() throws IOException {
+        if (fetched) {
+            throw new IllegalStateException("Tried to call 
MediaDataExtractor.fetch() again.");
+        }
+
+        MWApi api = CommonsApplication.createMWApi();
+        ApiResult result = api.action("query")
+                .param("prop", "revisions")
+                .param("titles", filename)
+                .param("rvprop", "content")
+                .param("rvlimit", 1)
+                .param("rvgeneratexml", 1)
+                .get();
+
+        processResult(result);
+        fetched = true;
+    }
+
+    private void processResult(ApiResult result) throws IOException {
+
+        String wikiSource = 
result.getString("/api/query/pages/page/revisions/rev");
+        String parseTreeXmlSource = 
result.getString("/api/query/pages/page/revisions/rev/@parsetree");
+
+        // In-page category links are extracted from source, as XML doesn't 
cover [[links]]
+        extractCategories(wikiSource);
+
+        // Description template info is extracted from preprocessor XML
+        processWikiParseTree(parseTreeXmlSource);
+    }
+
+    /**
+     * We could fetch all category links from API, but we actually only want 
the ones
+     * directly in the page source so they're editable. In the future this may 
change.
+     *
+     * @param source wikitext source code
+     */
+    private void extractCategories(String source) {
+        Pattern regex = 
Pattern.compile("\\[\\[\\s*Category\\s*:([^]]*)\\s*\\]\\]", 
Pattern.CASE_INSENSITIVE);
+        Matcher matcher = regex.matcher(source);
+        while (matcher.find()) {
+            String cat = matcher.group(1).trim();
+            categories.add(cat);
+        }
+    }
+
+    private void processWikiParseTree(String source) throws IOException {
+        Document doc;
+        try {
+            DocumentBuilder docBuilder = 
DocumentBuilderFactory.newInstance().newDocumentBuilder();
+            doc = docBuilder.parse(new 
ByteArrayInputStream(source.getBytes("UTF-8")));
+        } catch (ParserConfigurationException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalStateException e) {
+            throw new IOException(e);
+        } catch (SAXException e) {
+            throw new IOException(e);
+        }
+        Node templateNode = findTemplate(doc.getDocumentElement(), 
"information");
+        if (templateNode != null) {
+            Node descriptionNode = findTemplateParameter(templateNode, 
"description");
+            descriptions = getMultilingualText(descriptionNode);
+
+            Node authorNode = findTemplateParameter(templateNode, "author");
+            author = Utils.getStringFromDOM(authorNode);
+        }
+    }
+
+    private Node findTemplate(Element parentNode, String title) throws 
IOException {
+        String ucTitle= Utils.capitalize(title);
+        NodeList nodes = parentNode.getChildNodes();
+        for (int i = 0; i < nodes.getLength(); i++) {
+            Node node = nodes.item(i);
+            if (node.getNodeName().equals("template")) {
+                String foundTitle = getTemplateTitle(node);
+                if (Utils.capitalize(foundTitle).equals(ucTitle)) {
+                    return node;
+                }
+            }
+        }
+        return null;
+    }
+
+    private String getTemplateTitle(Node templateNode) throws IOException {
+        NodeList nodes = templateNode.getChildNodes();
+        for (int i = 0; i < nodes.getLength(); i++) {
+            Node node = nodes.item(i);
+            if (node.getNodeName().equals("title")) {
+                return node.getTextContent().trim();
+            }
+        }
+        throw new IOException("Template has no title element.");
+    }
+
+    private static abstract class TemplateChildNodeComparator {
+        abstract public boolean match(Node node);
+    }
+
+    private Node findTemplateParameter(Node templateNode, String name) throws 
IOException {
+        final String theName = name;
+        return findTemplateParameter(templateNode, new 
TemplateChildNodeComparator() {
+            @Override
+            public boolean match(Node node) {
+                return 
(Utils.capitalize(node.getTextContent().trim()).equals(Utils.capitalize(theName)));
+            }
+        });
+    }
+
+    private Node findTemplateParameter(Node templateNode, int index) throws 
IOException {
+        final String theIndex = "" + index;
+        return findTemplateParameter(templateNode, new 
TemplateChildNodeComparator() {
+            @Override
+            public boolean match(Node node) {
+                Element el = (Element)node;
+                if (el.getTextContent().trim().equals(theIndex)) {
+                    return true;
+                } else if (el.getAttribute("index") != null && 
el.getAttribute("index").trim().equals(theIndex)) {
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        });
+    }
+
+    private Node findTemplateParameter(Node templateNode, 
TemplateChildNodeComparator comparator) throws IOException {
+        NodeList nodes = templateNode.getChildNodes();
+        for (int i = 0; i < nodes.getLength(); i++) {
+            Node node = nodes.item(i);
+            if (node.getNodeName().equals("part")) {
+                NodeList childNodes = node.getChildNodes();
+                for (int j = 0; j < childNodes.getLength(); j++) {
+                    Node childNode = childNodes.item(j);
+                    if (childNode.getNodeName().equals("name")) {
+                        if (comparator.match(childNode)) {
+                            // yay! Now fetch the value node.
+                            for (int k = j + 1; k < childNodes.getLength(); 
k++) {
+                                Node siblingNode = childNodes.item(k);
+                                if (siblingNode.getNodeName().equals("value")) 
{
+                                    return siblingNode;
+                                }
+                            }
+                            throw new IOException("No value node found for 
matched template parameter.");
+                        }
+                    }
+                }
+            }
+        }
+        throw new IOException("No matching template parameter node found.");
+    }
+
+    // Extract a dictionary of multilingual texts from a subset of the parse 
tree.
+    // Texts are wrapped in things like {{en|foo} or {{en|1=foo bar}}.
+    // Text outside those wrappers is stuffed into a 'default' faux language 
key if present.
+    private Map<String, String> getMultilingualText(Node parentNode) throws 
IOException {
+        Map<String, String> texts = new HashMap<String, String>();
+        StringBuilder localText = new StringBuilder();
+
+        NodeList nodes = parentNode.getChildNodes();
+        for (int i = 0; i < nodes.getLength(); i++) {
+            Node node = nodes.item(i);
+            if (node.getNodeName().equals("template")) {
+                // process a template node
+                String title = getTemplateTitle(node);
+                if (title.length() < 3) {
+                    // Hopefully a language code. Nasty hack!
+                    String lang = title;
+                    Node valueNode = findTemplateParameter(node, 1);
+                    String value = Utils.getStringFromDOM(valueNode); // hope 
there's no subtemplates or formatting for now
+                    texts.put(lang, value);
+                }
+            } else if (node.getNodeType() == Node.TEXT_NODE) {
+                localText.append(node.getTextContent());
+            }
+        }
+
+        // Some descriptions don't list multilingual variants
+        String defaultText = localText.toString().trim();
+        if (defaultText.length() > 0) {
+            texts.put("default", localText.toString());
+        }
+        return texts;
+    }
+
+    /**
+     * Take our metadata and inject it into a live Media object.
+     * Media object might contain stale or cached data, or emptiness.
+     * @param media
+     */
+    public void fill(Media media) {
+        if (!fetched) {
+            throw new IllegalStateException("Tried to call 
MediaDataExtractor.fill() before fetch().");
+        }
+
+        media.setCategories(categories);
+        media.setDescriptions(descriptions);
+
+        // add author, date, etc fields
+    }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/Utils.java 
b/commons/src/main/java/org/wikimedia/commons/Utils.java
index b3623c1..f3bea5a 100644
--- a/commons/src/main/java/org/wikimedia/commons/Utils.java
+++ b/commons/src/main/java/org/wikimedia/commons/Utils.java
@@ -1,5 +1,6 @@
 package org.wikimedia.commons;
 
+import android.net.Uri;
 import android.os.*;
 import com.nostra13.universalimageloader.core.*;
 import com.nostra13.universalimageloader.core.assist.ImageScaleType;
@@ -174,4 +175,23 @@
         throw new RuntimeException("Unrecognized license value");
     }
 
+    public static String implode(String glue, Iterable<String> pieces) {
+        StringBuffer buffer = new StringBuffer();
+        boolean first = true;
+        for (String piece : pieces) {
+            if (first) {
+                first = false;
+            } else {
+                buffer.append(glue);
+            }
+            buffer.append(pieces);
+        }
+        return buffer.toString();
+    }
+
+    public static Uri uriForWikiPage(String name) {
+        String underscored = name.trim().replace(" ", "_");
+        String uriStr = CommonsApplication.HOME_URL + urlEncode(underscored);
+        return Uri.parse(uriStr);
+    }
 }
diff --git 
a/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java 
b/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
index f0f5a24..e57ee7b 100644
--- 
a/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
+++ 
b/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
@@ -204,6 +204,7 @@
     }
 
     public Contribution() {
+        super();
         timestamp = new Date(System.currentTimeMillis());
     }
 
diff --git 
a/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java 
b/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java
index 39343d8..e06d441 100644
--- a/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java
+++ b/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java
@@ -1,9 +1,12 @@
 package org.wikimedia.commons.media;
 
+import android.content.Intent;
 import android.graphics.*;
+import android.net.Uri;
 import android.os.*;
 import android.text.*;
 import android.util.Log;
+import android.util.TypedValue;
 import android.view.*;
 import android.widget.*;
 import com.actionbarsherlock.app.SherlockFragment;
@@ -14,7 +17,12 @@
 
 import com.android.volley.toolbox.*;
 
+import org.mediawiki.api.ApiResult;
+import org.mediawiki.api.MWApi;
 import org.wikimedia.commons.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
 
 public class MediaDetailFragment extends SherlockFragment {
 
@@ -33,6 +41,8 @@
         Bundle state = new Bundle();
         state.putBoolean("editable", editable);
         state.putInt("index", index);
+        state.putInt("listIndex", 0);
+        state.putInt("listTop", 0);
 
         mf.setArguments(state);
 
@@ -40,9 +50,22 @@
     }
 
     private ImageView image;
-    private EditText title;
+    //private EditText title;
     private ProgressBar loadingProgress;
     private ImageView loadingFailed;
+    private MediaDetailSpacer spacer;
+    private int initialListIndex = 0;
+    private int initialListTop = 0;
+
+    private TextView title;
+    private TextView desc;
+    private ListView listView;
+    private ArrayList<String> categoryNames;
+    private boolean categoriesLoaded = false;
+    private boolean categoriesPresent = false;
+    private ArrayAdapter categoryAdapter;
+    private ViewTreeObserver.OnGlobalLayoutListener observer; // for layout 
stuff, only used once!
+    private AsyncTask<Void,Void,Boolean> detailFetchTask;
 
 
     @Override
@@ -50,6 +73,20 @@
         super.onSaveInstanceState(outState);
         outState.putInt("index", index);
         outState.putBoolean("editable", editable);
+
+        getScrollPosition();
+        outState.putInt("listIndex", initialListIndex);
+        outState.putInt("listTop", initialListTop);
+    }
+
+    private void getScrollPosition() {
+        int initialListIndex = listView.getFirstVisiblePosition();
+        View firstVisibleItem = listView.getChildAt(initialListIndex);
+        if (firstVisibleItem == null) {
+            initialListTop = 0;
+        } else {
+            initialListTop = firstVisibleItem.getTop();
+        }
     }
 
     @Override
@@ -59,19 +96,46 @@
         if(savedInstanceState != null) {
             editable = savedInstanceState.getBoolean("editable");
             index = savedInstanceState.getInt("index");
+            initialListIndex = savedInstanceState.getInt("listIndex");
+            initialListTop = savedInstanceState.getInt("listTop");
         } else {
             editable = getArguments().getBoolean("editable");
             index = getArguments().getInt("index");
         }
         final Media media = detailProvider.getMediaAtPosition(index);
+        categoryNames = new ArrayList<String>();
+        categoryNames.add(getString(R.string.detail_panel_cats_loading));
 
-        View view = inflater.inflate(R.layout.fragment_media_detail, 
container, false);
+        final View view = inflater.inflate(R.layout.fragment_media_detail, 
container, false);
+
         image = (ImageView) view.findViewById(R.id.mediaDetailImage);
-        title = (EditText) view.findViewById(R.id.mediaDetailTitle);
         loadingProgress = (ProgressBar) 
view.findViewById(R.id.mediaDetailImageLoading);
         loadingFailed = (ImageView) 
view.findViewById(R.id.mediaDetailImageFailed);
+        listView = (ListView) view.findViewById(R.id.mediaDetailListView);
+
+        // Detail consists of a list view with main pane in header view, plus 
category list.
+        View detailView = 
getActivity().getLayoutInflater().inflate(R.layout.detail_main_panel, null, 
false);
+        listView.addHeaderView(detailView, null, false);
+        categoryAdapter = new ArrayAdapter(getActivity(), 
R.layout.detail_category_item, categoryNames);
+        listView.setAdapter(categoryAdapter);
+        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            public void onItemClick(AdapterView<?> adapterView, View view, int 
position, long id) {
+                if (categoriesLoaded && categoriesPresent) {
+                    String selectedCategoryTitle = "Category:" + 
categoryNames.get(position - 1);
+                    Intent viewIntent = new Intent();
+                    viewIntent.setAction(Intent.ACTION_VIEW);
+                    
viewIntent.setData(Utils.uriForWikiPage(selectedCategoryTitle));
+                    startActivity(viewIntent);
+                }
+            }
+        });
+
+        spacer = (MediaDetailSpacer) 
detailView.findViewById(R.id.mediaDetailSpacer);
+        title = (TextView) detailView.findViewById(R.id.mediaDetailTitle);
+        desc = (TextView) detailView.findViewById(R.id.mediaDetailDesc);
 
         // Enable or disable editing on the title
+        /*
         title.setClickable(editable);
         title.setFocusable(editable);
         title.setCursorVisible(editable);
@@ -79,6 +143,8 @@
         if(!editable) {
             title.setBackgroundDrawable(null);
         }
+        */
+
 
         String actualUrl = TextUtils.isEmpty(media.getImageUrl()) ? 
media.getLocalUri().toString() : media.getThumbnailUrl(640);
         if(actualUrl.startsWith("http")) {
@@ -88,6 +154,56 @@
             mwImage.setMedia(media, loader);
             Log.d("Volley", actualUrl);
             // FIXME: For transparent images
+
+            // Load image metadata: desc, license, categories
+            // FIXME: keep the spinner going while we load data
+            // FIXME: cache this data
+            detailFetchTask = new AsyncTask<Void, Void, Boolean>() {
+                private MediaDataExtractor extractor;
+
+                @Override
+                protected void onPreExecute() {
+                    extractor = new MediaDataExtractor(media.getFilename());
+                }
+
+                @Override
+                protected Boolean doInBackground(Void... voids) {
+                    try {
+                        extractor.fetch();
+                        return Boolean.TRUE;
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                    return Boolean.FALSE;
+                }
+
+                @Override
+                protected void onPostExecute(Boolean success) {
+                    detailFetchTask = null;
+
+                    if (success.booleanValue()) {
+                        extractor.fill(media);
+
+                        // Fill some fields
+                        desc.setText(media.getDescription("en"));
+
+                        categoryNames.removeAll(categoryNames);
+                        categoryNames.addAll(media.getCategories());
+
+                        categoriesLoaded = true;
+                        categoriesPresent = (categoryNames.size() > 0);
+                        if (!categoriesPresent) {
+                            // Stick in a filler element.
+                            
categoryNames.add(getString(R.string.detail_panel_cats_none));
+                        }
+
+                        categoryAdapter.notifyDataSetChanged();
+                    } else {
+                        Log.d("Commons", "Failed to load photo details.");
+                    }
+                }
+            };
+            Utils.executeAsyncTask(detailFetchTask);
         } else {
             
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl,
 image, displayOptions, new ImageLoadingListener() {
                 public void onLoadingStarted(String s, View view) {
@@ -113,8 +229,11 @@
                 }
             });
         }
-        title.setText(media.getDisplayTitle());
 
+        title.setText(media.getDisplayTitle());
+        desc.setText("");
+
+        /*
         title.addTextChangedListener(new TextWatcher() {
             public void beforeTextChanged(CharSequence charSequence, int i, 
int i2, int i3) {
 
@@ -130,6 +249,35 @@
 
             }
         });
+        */
+
+        // Layout observer to size the spacer item relative to the available 
space.
+        // There may be a .... better way to do this.
+        observer = new ViewTreeObserver.OnGlobalLayoutListener() {
+            private int currentHeight = -1;
+
+            public void onGlobalLayout() {
+                int viewHeight = view.getHeight();
+                //int textHeight = title.getLineHeight();
+                int paddingDp = 48;
+                float paddingPx = 
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingDp, 
getResources().getDisplayMetrics());
+                int newHeight = viewHeight - Math.round(paddingPx);
+
+                if (newHeight != currentHeight) {
+                    currentHeight = newHeight;
+                    ViewGroup.LayoutParams params = spacer.getLayoutParams();
+                    params.height = newHeight;
+                    spacer.setLayoutParams(params);
+
+                    // hack hack to trigger relayout
+                    categoryAdapter.notifyDataSetChanged();
+
+                    listView.setSelectionFromTop(initialListIndex, 
initialListTop);
+                }
+
+            }
+        };
+        view.getViewTreeObserver().addOnGlobalLayoutListener(observer);
         return view;
     }
 
@@ -139,4 +287,17 @@
 
         displayOptions = Utils.getGenericDisplayOptions().build();
     }
+
+    @Override
+    public void onDestroyView() {
+        if (detailFetchTask != null) {
+            detailFetchTask.cancel(true);
+            detailFetchTask = null;
+        }
+        if (observer != null) {
+            
getView().getViewTreeObserver().removeGlobalOnLayoutListener(observer); // old 
Android was on crack. CRACK IS WHACK
+            observer = null;
+        }
+        super.onDestroyView();
+    }
 }
diff --git 
a/commons/src/main/java/org/wikimedia/commons/media/MediaDetailSpacer.java 
b/commons/src/main/java/org/wikimedia/commons/media/MediaDetailSpacer.java
new file mode 100644
index 0000000..49b0485
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/media/MediaDetailSpacer.java
@@ -0,0 +1,19 @@
+package org.wikimedia.commons.media;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+public class MediaDetailSpacer extends View {
+    public MediaDetailSpacer(Context context) {
+        super(context);
+    }
+
+    public MediaDetailSpacer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MediaDetailSpacer(Context context, AttributeSet attrs, int 
defStyle) {
+        super(context, attrs, defStyle);
+    }
+}

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I46d0a77481dbe64a268a72f3efe49ae72168541f
Gerrit-PatchSet: 1
Gerrit-Project: apps/android/commons
Gerrit-Branch: master
Gerrit-Owner: Yuvipanda <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>

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

Reply via email to