AppCompatDelegateImplBase.java revision 0f58401dc72db8ea2a201e3c0220f6529f2a7b33
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.support.v7.app;
18
19import android.app.Activity;
20import android.app.Dialog;
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.graphics.drawable.Drawable;
24import android.os.Build;
25import android.os.Bundle;
26import android.support.v7.appcompat.R;
27import android.support.v7.internal.view.SupportMenuInflater;
28import android.support.v7.internal.view.WindowCallbackWrapper;
29import android.support.v7.internal.view.menu.MenuBuilder;
30import android.support.v7.internal.widget.TintTypedArray;
31import android.support.v7.view.ActionMode;
32import android.view.KeyEvent;
33import android.view.Menu;
34import android.view.MenuInflater;
35import android.view.View;
36import android.view.Window;
37
38abstract class AppCompatDelegateImplBase extends AppCompatDelegate {
39
40    final Context mContext;
41    final Window mWindow;
42    final Window.Callback mOriginalWindowCallback;
43    final AppCompatCallback mAppCompatCallback;
44
45    private ActionBar mActionBar;
46    private MenuInflater mMenuInflater;
47
48    // true if this activity has an action bar.
49    boolean mHasActionBar;
50    // true if this activity's action bar overlays other activity content.
51    boolean mOverlayActionBar;
52    // true if this any action modes should overlay the activity content
53    boolean mOverlayActionMode;
54    // true if this activity is floating (e.g. Dialog)
55    boolean mIsFloating;
56    // true if this activity has no title
57    boolean mWindowNoTitle;
58
59    private CharSequence mTitle;
60
61    private boolean mIsDestroyed;
62
63    AppCompatDelegateImplBase(Context context, Window window, AppCompatCallback callback) {
64        mContext = context;
65        mWindow = window;
66        mAppCompatCallback = callback;
67
68        mOriginalWindowCallback = mWindow.getCallback();
69        if (mOriginalWindowCallback instanceof AppCompatWindowCallback) {
70            throw new IllegalStateException(
71                    "AppCompat has already installed itself into the Window");
72        }
73        // Now install the new callback
74        mWindow.setCallback(new AppCompatWindowCallback(mOriginalWindowCallback));
75    }
76
77    abstract ActionBar createSupportActionBar();
78
79    @Override
80    public ActionBar getSupportActionBar() {
81        // The Action Bar should be lazily created as hasActionBar
82        // could change after onCreate
83        if (mHasActionBar) {
84            if (mActionBar == null) {
85                mActionBar = createSupportActionBar();
86            }
87        }
88        return mActionBar;
89    }
90
91    final ActionBar peekSupportActionBar() {
92        return mActionBar;
93    }
94
95    final void setSupportActionBar(ActionBar actionBar) {
96        mActionBar = actionBar;
97    }
98
99    @Override
100    public MenuInflater getMenuInflater() {
101        if (mMenuInflater == null) {
102            mMenuInflater = new SupportMenuInflater(getActionBarThemedContext());
103        }
104        return mMenuInflater;
105    }
106
107    @Override
108    public void onCreate(Bundle savedInstanceState) {
109        TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
110
111        if (!a.hasValue(R.styleable.Theme_windowActionBar)) {
112            a.recycle();
113            throw new IllegalStateException(
114                    "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
115        }
116
117        if (a.getBoolean(R.styleable.Theme_windowActionBar, false)) {
118            mHasActionBar = true;
119        }
120        if (a.getBoolean(R.styleable.Theme_windowActionBarOverlay, false)) {
121            mOverlayActionBar = true;
122        }
123        if (a.getBoolean(R.styleable.Theme_windowActionModeOverlay, false)) {
124            mOverlayActionMode = true;
125        }
126        mIsFloating = a.getBoolean(R.styleable.Theme_android_windowIsFloating, false);
127        mWindowNoTitle = a.getBoolean(R.styleable.Theme_windowNoTitle, false);
128        a.recycle();
129    }
130
131    // Methods used to create and respond to options menu
132    abstract boolean onPanelClosed(int featureId, Menu menu);
133
134    abstract boolean onMenuOpened(int featureId, Menu menu);
135
136    abstract boolean dispatchKeyEvent(KeyEvent event);
137
138    abstract boolean onKeyShortcut(int keyCode, KeyEvent event);
139
140    @Override
141    public final ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
142        return new ActionBarDrawableToggleImpl();
143    }
144
145    final Context getActionBarThemedContext() {
146        Context context = null;
147
148        // If we have an action bar, let it return a themed context
149        ActionBar ab = getSupportActionBar();
150        if (ab != null) {
151            context = ab.getThemedContext();
152        }
153
154        if (context == null) {
155            context = mContext;
156        }
157        return context;
158    }
159
160    private class ActionBarDrawableToggleImpl implements ActionBarDrawerToggle.Delegate {
161        @Override
162        public Drawable getThemeUpIndicator() {
163            final TintTypedArray a = TintTypedArray.obtainStyledAttributes(
164                    getActionBarThemedContext(), null, new int[]{ R.attr.homeAsUpIndicator });
165            final Drawable result = a.getDrawable(0);
166            a.recycle();
167            return result;
168        }
169
170        @Override
171        public Context getActionBarThemedContext() {
172            return AppCompatDelegateImplBase.this.getActionBarThemedContext();
173        }
174
175        @Override
176        public boolean isNavigationVisible() {
177            final ActionBar ab = getSupportActionBar();
178            return ab != null && (ab.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0;
179        }
180
181        @Override
182        public void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
183            ActionBar ab = getSupportActionBar();
184            if (ab != null) {
185                ab.setHomeAsUpIndicator(upDrawable);
186                ab.setHomeActionContentDescription(contentDescRes);
187            }
188        }
189
190        @Override
191        public void setActionBarDescription(int contentDescRes) {
192            ActionBar ab = getSupportActionBar();
193            if (ab != null) {
194                ab.setHomeActionContentDescription(contentDescRes);
195            }
196        }
197    }
198
199    abstract ActionMode startSupportActionModeFromWindow(ActionMode.Callback callback);
200
201    @Override
202    public final void onDestroy() {
203        mIsDestroyed = true;
204    }
205
206    final boolean isDestroyed() {
207        return mIsDestroyed;
208    }
209
210    final Window.Callback getWindowCallback() {
211        return mWindow.getCallback();
212    }
213
214    @Override
215    public final void setTitle(CharSequence title) {
216        mTitle = title;
217        onTitleChanged(title);
218    }
219
220    abstract void onTitleChanged(CharSequence title);
221
222    final CharSequence getTitle() {
223        // If the original window callback is an Activity, we'll use it's title
224        if (mOriginalWindowCallback instanceof Activity) {
225            return ((Activity) mOriginalWindowCallback).getTitle();
226        }
227        // Else, we'll return the title we have recorded ourselves
228        return mTitle;
229    }
230
231    private class AppCompatWindowCallback extends WindowCallbackWrapper {
232        AppCompatWindowCallback(Window.Callback callback) {
233            super(callback);
234        }
235
236        @Override
237        public boolean dispatchKeyEvent(KeyEvent event) {
238            if (AppCompatDelegateImplBase.this.dispatchKeyEvent(event)) {
239                return true;
240            }
241            return super.dispatchKeyEvent(event);
242        }
243
244        @Override
245        public boolean onCreatePanelMenu(int featureId, Menu menu) {
246            if (featureId == Window.FEATURE_OPTIONS_PANEL && !(menu instanceof MenuBuilder)) {
247                // If this is an options menu but it's not an AppCompat menu, we eat the event
248                // and return false
249                return false;
250            }
251            return super.onCreatePanelMenu(featureId, menu);
252        }
253
254        @Override
255        public boolean onPreparePanel(int featureId, View view, Menu menu) {
256            if (featureId == Window.FEATURE_OPTIONS_PANEL && !(menu instanceof MenuBuilder)) {
257                // If this is an options menu but it's not an AppCompat menu, we eat the event
258                // and return false
259                return false;
260            }
261
262            if (featureId == Window.FEATURE_OPTIONS_PANEL && bypassPrepareOptionsPanelIfNeeded()) {
263                // If this is an options menu and we need to bypass onPreparePanel, do so
264                if (mOriginalWindowCallback instanceof Activity) {
265                    return ((Activity) mOriginalWindowCallback).onPrepareOptionsMenu(menu);
266                } else if (mOriginalWindowCallback instanceof Dialog) {
267                    return ((Dialog) mOriginalWindowCallback).onPrepareOptionsMenu(menu);
268                }
269                return false;
270            }
271
272            // Else, defer to the default handling
273            return super.onPreparePanel(featureId, view, menu);
274        }
275
276        @Override
277        public boolean onMenuOpened(int featureId, Menu menu) {
278            if (AppCompatDelegateImplBase.this.onMenuOpened(featureId, menu)) {
279                return true;
280            }
281            return super.onMenuOpened(featureId, menu);
282        }
283
284        @Override
285        public boolean dispatchKeyShortcutEvent(KeyEvent event) {
286            if (AppCompatDelegateImplBase.this.onKeyShortcut(event.getKeyCode(), event)) {
287                return true;
288            }
289            return super.dispatchKeyShortcutEvent(event);
290        }
291
292        @Override
293        public void onContentChanged() {
294            // We purposely do not propagate this call as this is called when we install
295            // our sub-decor rather than the user's content
296        }
297
298        @Override
299        public void onPanelClosed(int featureId, Menu menu) {
300            if (AppCompatDelegateImplBase.this.onPanelClosed(featureId, menu)) {
301                return;
302            }
303            super.onPanelClosed(featureId, menu);
304        }
305
306        /**
307         * For the options menu, we may need to call onPrepareOptionsMenu() directly,
308         * bypassing onPreparePanel(). This is because onPreparePanel() in certain situations
309         * calls menu.hasVisibleItems(), which interferes with any initial invisible items.
310         *
311         * @return true if onPrepareOptionsMenu should be called directly.
312         */
313        private boolean bypassPrepareOptionsPanelIfNeeded() {
314            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN
315                    && mOriginalWindowCallback instanceof Activity) {
316                // For Activities, we only need to bypass onPreparePanel if we're running pre-JB
317                return true;
318            } else if (mOriginalWindowCallback instanceof Dialog) {
319                // For Dialogs, we always need to bypass onPreparePanel
320                return true;
321            }
322            return false;
323        }
324    }
325}
326