1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.transition;
18
19import android.annotation.TransitionRes;
20import android.content.Context;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.content.res.XmlResourceParser;
24import android.util.ArrayMap;
25import android.util.AttributeSet;
26import android.util.Xml;
27import android.view.InflateException;
28import android.view.ViewGroup;
29
30import com.android.internal.R;
31
32import org.xmlpull.v1.XmlPullParser;
33import org.xmlpull.v1.XmlPullParserException;
34
35import java.io.IOException;
36import java.lang.reflect.Constructor;
37import java.lang.reflect.InvocationTargetException;
38
39/**
40 * This class inflates scenes and transitions from resource files.
41 *
42 * Information on XML resource descriptions for transitions can be found for
43 * {@link android.R.styleable#Transition}, {@link android.R.styleable#TransitionSet},
44 * {@link android.R.styleable#TransitionTarget}, {@link android.R.styleable#Fade},
45 * and {@link android.R.styleable#TransitionManager}.
46 */
47public class TransitionInflater {
48
49    private static final Class<?>[] sConstructorSignature = new Class[] {
50            Context.class, AttributeSet.class};
51    private final static ArrayMap<String, Constructor> sConstructors =
52            new ArrayMap<String, Constructor>();
53
54    private Context mContext;
55
56    private TransitionInflater(Context context) {
57        mContext = context;
58    }
59
60    /**
61     * Obtains the TransitionInflater from the given context.
62     */
63    public static TransitionInflater from(Context context) {
64        return new TransitionInflater(context);
65    }
66
67    /**
68     * Loads a {@link Transition} object from a resource
69     *
70     * @param resource The resource id of the transition to load
71     * @return The loaded Transition object
72     * @throws android.content.res.Resources.NotFoundException when the
73     * transition cannot be loaded
74     */
75    public Transition inflateTransition(@TransitionRes int resource) {
76        //noinspection ResourceType
77        XmlResourceParser parser =  mContext.getResources().getXml(resource);
78        try {
79            return createTransitionFromXml(parser, Xml.asAttributeSet(parser), null);
80        } catch (XmlPullParserException e) {
81            InflateException ex = new InflateException(e.getMessage());
82            ex.initCause(e);
83            throw ex;
84        } catch (IOException e) {
85            InflateException ex = new InflateException(
86                    parser.getPositionDescription()
87                            + ": " + e.getMessage());
88            ex.initCause(e);
89            throw ex;
90        } finally {
91            parser.close();
92        }
93    }
94
95    /**
96     * Loads a {@link TransitionManager} object from a resource
97     *
98     * @param resource The resource id of the transition manager to load
99     * @return The loaded TransitionManager object
100     * @throws android.content.res.Resources.NotFoundException when the
101     * transition manager cannot be loaded
102     */
103    public TransitionManager inflateTransitionManager(@TransitionRes int resource,
104            ViewGroup sceneRoot) {
105        //noinspection ResourceType
106        XmlResourceParser parser =  mContext.getResources().getXml(resource);
107        try {
108            return createTransitionManagerFromXml(parser, Xml.asAttributeSet(parser), sceneRoot);
109        } catch (XmlPullParserException e) {
110            InflateException ex = new InflateException(e.getMessage());
111            ex.initCause(e);
112            throw ex;
113        } catch (IOException e) {
114            InflateException ex = new InflateException(
115                    parser.getPositionDescription()
116                            + ": " + e.getMessage());
117            ex.initCause(e);
118            throw ex;
119        } finally {
120            parser.close();
121        }
122    }
123
124    //
125    // Transition loading
126    //
127    private Transition createTransitionFromXml(XmlPullParser parser,
128            AttributeSet attrs, Transition parent)
129            throws XmlPullParserException, IOException {
130
131        Transition transition = null;
132
133        // Make sure we are on a start tag.
134        int type;
135        int depth = parser.getDepth();
136
137        TransitionSet transitionSet = (parent instanceof TransitionSet)
138                ? (TransitionSet) parent : null;
139
140        while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
141                && type != XmlPullParser.END_DOCUMENT) {
142
143            if (type != XmlPullParser.START_TAG) {
144                continue;
145            }
146
147            String  name = parser.getName();
148            if ("fade".equals(name)) {
149                transition = new Fade(mContext, attrs);
150            } else if ("changeBounds".equals(name)) {
151                transition = new ChangeBounds(mContext, attrs);
152            } else if ("slide".equals(name)) {
153                transition = new Slide(mContext, attrs);
154            } else if ("explode".equals(name)) {
155                transition = new Explode(mContext, attrs);
156            } else if ("changeImageTransform".equals(name)) {
157                transition = new ChangeImageTransform(mContext, attrs);
158            } else if ("changeTransform".equals(name)) {
159                transition = new ChangeTransform(mContext, attrs);
160            } else if ("changeClipBounds".equals(name)) {
161                transition = new ChangeClipBounds(mContext, attrs);
162            } else if ("autoTransition".equals(name)) {
163                transition = new AutoTransition(mContext, attrs);
164            } else if ("recolor".equals(name)) {
165                transition = new Recolor(mContext, attrs);
166            } else if ("changeScroll".equals(name)) {
167                transition = new ChangeScroll(mContext, attrs);
168            } else if ("transitionSet".equals(name)) {
169                transition = new TransitionSet(mContext, attrs);
170            } else if ("transition".equals(name)) {
171                transition = (Transition) createCustom(attrs, Transition.class, "transition");
172            } else if ("targets".equals(name)) {
173                getTargetIds(parser, attrs, parent);
174            } else if ("arcMotion".equals(name)) {
175                parent.setPathMotion(new ArcMotion(mContext, attrs));
176            } else if ("pathMotion".equals(name)) {
177                parent.setPathMotion((PathMotion)createCustom(attrs, PathMotion.class, "pathMotion"));
178            } else if ("patternPathMotion".equals(name)) {
179                parent.setPathMotion(new PatternPathMotion(mContext, attrs));
180            } else {
181                throw new RuntimeException("Unknown scene name: " + parser.getName());
182            }
183            if (transition != null) {
184                if (!parser.isEmptyElementTag()) {
185                    createTransitionFromXml(parser, attrs, transition);
186                }
187                if (transitionSet != null) {
188                    transitionSet.addTransition(transition);
189                    transition = null;
190                } else if (parent != null) {
191                    throw new InflateException("Could not add transition to another transition.");
192                }
193            }
194        }
195
196        return transition;
197    }
198
199    private Object createCustom(AttributeSet attrs, Class expectedType, String tag) {
200        String className = attrs.getAttributeValue(null, "class");
201
202        if (className == null) {
203            throw new InflateException(tag + " tag must have a 'class' attribute");
204        }
205
206        try {
207            synchronized (sConstructors) {
208                Constructor constructor = sConstructors.get(className);
209                if (constructor == null) {
210                    Class c = mContext.getClassLoader().loadClass(className)
211                            .asSubclass(expectedType);
212                    if (c != null) {
213                        constructor = c.getConstructor(sConstructorSignature);
214                        constructor.setAccessible(true);
215                        sConstructors.put(className, constructor);
216                    }
217                }
218                return constructor.newInstance(mContext, attrs);
219            }
220        } catch (InstantiationException e) {
221            throw new InflateException("Could not instantiate " + expectedType + " class " +
222                    className, e);
223        } catch (ClassNotFoundException e) {
224            throw new InflateException("Could not instantiate " + expectedType + " class " +
225                    className, e);
226        } catch (InvocationTargetException e) {
227            throw new InflateException("Could not instantiate " + expectedType + " class " +
228                    className, e);
229        } catch (NoSuchMethodException e) {
230            throw new InflateException("Could not instantiate " + expectedType + " class " +
231                    className, e);
232        } catch (IllegalAccessException e) {
233            throw new InflateException("Could not instantiate " + expectedType + " class " +
234                    className, e);
235        }
236    }
237
238    private void getTargetIds(XmlPullParser parser,
239            AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException {
240
241        // Make sure we are on a start tag.
242        int type;
243        int depth = parser.getDepth();
244
245        while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
246                && type != XmlPullParser.END_DOCUMENT) {
247
248            if (type != XmlPullParser.START_TAG) {
249                continue;
250            }
251
252            String  name = parser.getName();
253            if (name.equals("target")) {
254                TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TransitionTarget);
255                int id = a.getResourceId(R.styleable.TransitionTarget_targetId, 0);
256                String transitionName;
257                if (id != 0) {
258                    transition.addTarget(id);
259                } else if ((id = a.getResourceId(R.styleable.TransitionTarget_excludeId, 0)) != 0) {
260                    transition.excludeTarget(id, true);
261                } else if ((transitionName = a.getString(R.styleable.TransitionTarget_targetName))
262                        != null) {
263                    transition.addTarget(transitionName);
264                } else if ((transitionName = a.getString(R.styleable.TransitionTarget_excludeName))
265                        != null) {
266                    transition.excludeTarget(transitionName, true);
267                } else {
268                    String className = a.getString(R.styleable.TransitionTarget_excludeClass);
269                    try {
270                        if (className != null) {
271                            Class clazz = Class.forName(className);
272                            transition.excludeTarget(clazz, true);
273                        } else if ((className =
274                                a.getString(R.styleable.TransitionTarget_targetClass)) != null) {
275                            Class clazz = Class.forName(className);
276                            transition.addTarget(clazz);
277                        }
278                    } catch (ClassNotFoundException e) {
279                        a.recycle();
280                        throw new RuntimeException("Could not create " + className, e);
281                    }
282                }
283                a.recycle();
284            } else {
285                throw new RuntimeException("Unknown scene name: " + parser.getName());
286            }
287        }
288    }
289
290    //
291    // TransitionManager loading
292    //
293
294    private TransitionManager createTransitionManagerFromXml(XmlPullParser parser,
295            AttributeSet attrs, ViewGroup sceneRoot) throws XmlPullParserException, IOException {
296
297        // Make sure we are on a start tag.
298        int type;
299        int depth = parser.getDepth();
300        TransitionManager transitionManager = null;
301
302        while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
303                && type != XmlPullParser.END_DOCUMENT) {
304
305            if (type != XmlPullParser.START_TAG) {
306                continue;
307            }
308
309            String  name = parser.getName();
310            if (name.equals("transitionManager")) {
311                transitionManager = new TransitionManager();
312            } else if (name.equals("transition") && (transitionManager != null)) {
313                loadTransition(attrs, sceneRoot, transitionManager);
314            } else {
315                throw new RuntimeException("Unknown scene name: " + parser.getName());
316            }
317        }
318        return transitionManager;
319    }
320
321    private void loadTransition(AttributeSet attrs, ViewGroup sceneRoot,
322            TransitionManager transitionManager) throws Resources.NotFoundException {
323
324        TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TransitionManager);
325        int transitionId = a.getResourceId(R.styleable.TransitionManager_transition, -1);
326        int fromId = a.getResourceId(R.styleable.TransitionManager_fromScene, -1);
327        Scene fromScene = (fromId < 0) ? null: Scene.getSceneForLayout(sceneRoot, fromId, mContext);
328        int toId = a.getResourceId(R.styleable.TransitionManager_toScene, -1);
329        Scene toScene = (toId < 0) ? null : Scene.getSceneForLayout(sceneRoot, toId, mContext);
330
331        if (transitionId >= 0) {
332            Transition transition = inflateTransition(transitionId);
333            if (transition != null) {
334                if (toScene == null) {
335                    throw new RuntimeException("No toScene for transition ID " + transitionId);
336                }
337                if (fromScene == null) {
338                    transitionManager.setTransition(toScene, transition);
339                } else {
340                    transitionManager.setTransition(fromScene, toScene, transition);
341                }
342            }
343        }
344        a.recycle();
345    }
346}
347