• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 
17 package com.android.phone;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.Canvas;
22 import android.graphics.Rect;
23 import android.graphics.drawable.Drawable;
24 import android.util.AttributeSet;
25 import android.util.Log;
26 import android.view.KeyEvent;
27 import android.view.View;
28 import android.view.ViewGroup;
29 
30 import java.util.ArrayList;
31 
32 
33 /**
34  * Custom View used as the "options panel" for the InCallScreen
35  * (i.e. the standard menu triggered by the MENU button.)
36  *
37  * This class purely handles the layout and display of the in-call menu
38  * items, *not* the actual contents of the menu or the states of the
39  * items.  (See InCallMenu for the corresponding "model" class.)
40 
41  */
42 class InCallMenuView extends ViewGroup {
43     private static final String LOG_TAG = "PHONE/InCallMenuView";
44     private static final boolean DBG = false;
45 
46     private int mRowHeight;
47 
48     /** Divider that is drawn between all rows */
49     private Drawable mHorizontalDivider;
50     /** Height of the horizontal divider */
51     private int mHorizontalDividerHeight;
52     /** Set of horizontal divider positions where the horizontal divider will be drawn */
53     private ArrayList<Rect> mHorizontalDividerRects;
54 
55     /** Divider that is drawn between all columns */
56     private Drawable mVerticalDivider;
57     /** Width of the vertical divider */
58     private int mVerticalDividerWidth;
59     /** Set of vertical divider positions where the vertical divider will be drawn */
60     private ArrayList<Rect> mVerticalDividerRects;
61 
62     /** Background of each item (should contain the selected and focused states) */
63     private Drawable mItemBackground;
64 
65     /**
66      * The actual layout of items in the menu, organized into 3 rows.
67      *
68      * Row 0 is the topmost row onscreen, item 0 is the leftmost item in a row.
69      *
70      * Individual items may be disabled or hidden, but never move between
71      * rows or change their order within a row.
72      */
73     private static final int NUM_ROWS = 3;
74     private static final int MAX_ITEMS_PER_ROW = 10;
75     private InCallMenuItemView[][] mItems = new InCallMenuItemView[NUM_ROWS][MAX_ITEMS_PER_ROW];
76 
77     private int mNumItemsForRow[] = new int[NUM_ROWS];
78 
79     /**
80      * Number of visible items per row, given the current state of all the
81      * menu items.
82      * A row with zero visible items isn't drawn at all.
83      */
84     private int mNumVisibleItemsForRow[] = new int[NUM_ROWS];
85     private int mNumVisibleRows;
86 
87     /**
88      * Reference to the InCallScreen activity that owns us.  This will be
89      * null if we haven't been initialized yet *or* after the InCallScreen
90      * activity has been destroyed.
91      */
92     private InCallScreen mInCallScreen;
93 
94 
InCallMenuView(Context context, InCallScreen inCallScreen)95     InCallMenuView(Context context, InCallScreen inCallScreen) {
96         super(context);
97         if (DBG) log("InCallMenuView constructor...");
98 
99         mInCallScreen = inCallScreen;
100 
101         // Look up a few styled attrs from IconMenuView and/or MenuView
102         // (to keep our look and feel at least *somewhat* consistent with
103         // menus in other apps.)
104 
105         TypedArray a =
106                 mContext.obtainStyledAttributes(com.android.internal.R.styleable.IconMenuView);
107         if (DBG) log("- IconMenuView styled attrs: " + a);
108         mRowHeight = a.getDimensionPixelSize(
109                 com.android.internal.R.styleable.IconMenuView_rowHeight, 64);
110         if (DBG) log("  - mRowHeight: " + mRowHeight);
111         a.recycle();
112 
113         a = mContext.obtainStyledAttributes(com.android.internal.R.styleable.MenuView);
114         if (DBG) log("- MenuView styled attrs: " + a);
115         mItemBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground);
116         if (DBG) log("  - mItemBackground: " + mItemBackground);
117         mHorizontalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider);
118         if (DBG) log("  - mHorizontalDivider: " + mHorizontalDivider);
119         mHorizontalDividerRects = new ArrayList<Rect>();
120         mVerticalDivider =  a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider);
121         if (DBG) log("  - mVerticalDivider: " + mVerticalDivider);
122         mVerticalDividerRects = new ArrayList<Rect>();
123         a.recycle();
124 
125         if (mHorizontalDivider != null) {
126             mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight();
127             // Make sure to have some height for the divider
128             if (mHorizontalDividerHeight == -1) mHorizontalDividerHeight = 1;
129         }
130 
131         if (mVerticalDivider != null) {
132             mVerticalDividerWidth = mVerticalDivider.getIntrinsicWidth();
133             // Make sure to have some width for the divider
134             if (mVerticalDividerWidth == -1) mVerticalDividerWidth = 1;
135         }
136 
137         // This view will be drawing the dividers.
138         setWillNotDraw(false);
139 
140         // Arrange to get key events even when there's no focused item in
141         // the in-call menu (i.e. when in touch mode).
142         // (We *always* want key events whenever we're visible, so that we
143         // can forward them to the InCallScreen activity; see dispatchKeyEvent().)
144         setFocusableInTouchMode(true);
145         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
146 
147         // The default ViewGroup.LayoutParams width and height are
148         // WRAP_CONTENT.  (This applies to us right now since we
149         // initially have no LayoutParams at all.)
150         // But in the Menu framework, when returning a view from
151         // onCreatePanelView(), a layout width of WRAP_CONTENT indicates
152         // that you want the smaller-sized "More" menu frame.  We want the
153         // full-screen-width menu frame instead, though, so we need to
154         // give ourselves a LayoutParams with width==MATCH_PARENT.
155         ViewGroup.LayoutParams lp =
156                 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
157                                            ViewGroup.LayoutParams.WRAP_CONTENT);
158         setLayoutParams(lp);
159     }
160 
161     /**
162      * Null out our reference to the InCallScreen activity.
163      * This indicates that the InCallScreen activity has been destroyed.
164      */
clearInCallScreenReference()165     void clearInCallScreenReference() {
166         mInCallScreen = null;
167     }
168 
169     /**
170      * Adds an InCallMenuItemView to the specified row.
171      */
addItemView(InCallMenuItemView itemView, int row)172     /* package */ void addItemView(InCallMenuItemView itemView, int row) {
173         if (DBG) log("addItemView(" + itemView + ", row " + row + ")...");
174 
175         if (row >= NUM_ROWS) {
176             throw new IllegalStateException("Row index " + row + " > NUM_ROWS");
177         }
178 
179         int indexInRow = mNumItemsForRow[row];
180         if (indexInRow >= MAX_ITEMS_PER_ROW) {
181             throw new IllegalStateException("Too many items (" + indexInRow + ") in row " + row);
182         }
183         mNumItemsForRow[row]++;
184         mItems[row][indexInRow] = itemView;
185 
186         //
187         // Finally, add this item as a child.
188         //
189 
190         ViewGroup.LayoutParams lp = itemView.getLayoutParams();
191 
192         if (lp == null) {
193             // Default layout parameters
194             lp = new LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.MATCH_PARENT);
195         }
196 
197         // Apply the background to the item view
198         itemView.setBackgroundDrawable(mItemBackground.getConstantState().newDrawable());
199 
200         addView(itemView, lp);
201     }
202 
203     /**
204      * Precomputes the number of visible items per row, and the total
205      * number of visible rows.  (A row with zero visible items isn't
206      * drawn at all.)
207      */
updateVisibility()208     /* package */ void updateVisibility() {
209         if (DBG) log("updateVisibility()...");
210 
211         mNumVisibleRows = 0;
212 
213         for (int row = 0; row < NUM_ROWS; row++) {
214             InCallMenuItemView[] thisRow = mItems[row];
215             int numItemsThisRow = mNumItemsForRow[row];
216             int numVisibleThisRow = 0;
217             for (int itemIndex = 0; itemIndex < numItemsThisRow; itemIndex++) {
218                 // if (DBG) log("  - Checking item: " + mItems[row][itemIndex]);
219                 if  (mItems[row][itemIndex].isVisible()) numVisibleThisRow++;
220             }
221             if (DBG) log("==> Num visible for row " + row + ": " + numVisibleThisRow);
222             mNumVisibleItemsForRow[row] = numVisibleThisRow;
223             if (numVisibleThisRow > 0) mNumVisibleRows++;
224         }
225         if (DBG) log("==> Num visible rows: " + mNumVisibleRows);
226     }
227 
dumpState()228     /* package */ void dumpState() {
229         if (DBG) log("============ dumpState() ============");
230         if (DBG) log("- mItems LENGTH: " + mItems.length);
231         for (int row = 0; row < NUM_ROWS; row++) {
232             if (DBG) log("-      Row " + row + ": length " + mItems[row].length
233                          + ", num items " + mNumItemsForRow[row]
234                          + ", num visible " + mNumVisibleItemsForRow[row]);
235         }
236     }
237 
238     /**
239      * The positioning algorithm that gets called from onMeasure.  It just
240      * computes positions for each child, and then stores them in the
241      * child's layout params.
242      *
243      * At this point the visibility of each item in mItems[][] is correct,
244      * and mNumVisibleRows and mNumVisibleItemsForRow[] have already been
245      * precomputed.
246      *
247      * @param menuWidth The width of this menu to assume for positioning
248      * @param menuHeight The height of this menu to assume for positioning
249      *
250      * TODO: This is a near-exact duplicate of IconMenuView.positionChildren().
251      * Consider abstracting this out into a more general-purpose "grid layout
252      * with dividers" container that both classes could use...
253      */
positionChildren(int menuWidth, int menuHeight)254     private void positionChildren(int menuWidth, int menuHeight) {
255         if (DBG) log("positionChildren(" + menuWidth + " x " + menuHeight + ")...");
256 
257         // Clear the containers for the positions where the dividers should be drawn
258         if (mHorizontalDivider != null) mHorizontalDividerRects.clear();
259         if (mVerticalDivider != null) mVerticalDividerRects.clear();
260 
261         InCallMenuItemView child;
262         InCallMenuView.LayoutParams childLayoutParams = null;
263 
264         // Use float for this to get precise positions (uniform item widths
265         // instead of last one taking any slack), and then convert to ints at last opportunity
266         float itemLeft;
267         float itemTop = 0;
268         // Since each row can have a different number of items, this will be computed per row
269         float itemWidth;
270         // Subtract the space needed for the horizontal dividers
271         final float itemHeight = (menuHeight - mHorizontalDividerHeight * (mNumVisibleRows - 1))
272                 / (float) mNumVisibleRows;
273 
274         // We add horizontal dividers between each visible row, so there should
275         // be a total of mNumVisibleRows-1 of them.
276         int numHorizDividersRemainingToDraw = mNumVisibleRows - 1;
277 
278         for (int row = 0; row < NUM_ROWS; row++) {
279             int numItemsThisRow = mNumItemsForRow[row];
280             int numVisibleThisRow = mNumVisibleItemsForRow[row];
281             if (DBG) log("  - num visible for row " + row + ": " + numVisibleThisRow);
282             if (numVisibleThisRow == 0) {
283                 continue;
284             }
285 
286             InCallMenuItemView[] thisRow = mItems[row];
287 
288             // Start at the left
289             itemLeft = 0;
290 
291             // Subtract the space needed for the vertical dividers, and
292             // divide by the number of items.
293             itemWidth = (menuWidth - mVerticalDividerWidth * (numVisibleThisRow - 1))
294                     / (float) numVisibleThisRow;
295 
296             for (int itemIndex = 0; itemIndex < numItemsThisRow; itemIndex++) {
297                 child = mItems[row][itemIndex];
298 
299                 if (!child.isVisible()) continue;
300 
301                 if (DBG) log("==> child [" + row + "][" + itemIndex + "]: " + child);
302 
303                 // Tell the child to be exactly this size
304                 child.measure(MeasureSpec.makeMeasureSpec((int) itemWidth, MeasureSpec.EXACTLY),
305                               MeasureSpec.makeMeasureSpec((int) itemHeight, MeasureSpec.EXACTLY));
306 
307                 // Remember the child's position for layout
308                 childLayoutParams = (InCallMenuView.LayoutParams) child.getLayoutParams();
309                 childLayoutParams.left = (int) itemLeft;
310                 childLayoutParams.right = (int) (itemLeft + itemWidth);
311                 childLayoutParams.top = (int) itemTop;
312                 childLayoutParams.bottom = (int) (itemTop + itemHeight);
313 
314                 // Increment by item width
315                 itemLeft += itemWidth;
316 
317                 // Add a vertical divider to draw
318                 if (mVerticalDivider != null) {
319                     mVerticalDividerRects.add(new Rect((int) itemLeft,
320                             (int) itemTop, (int) (itemLeft + mVerticalDividerWidth),
321                             (int) (itemTop + itemHeight)));
322                 }
323 
324                 // Increment by divider width (even if we're not computing
325                 // dividers, since we need to leave room for them when
326                 // calculating item positions)
327                 itemLeft += mVerticalDividerWidth;
328             }
329 
330             // Last child on each row should extend to very right edge
331             if (childLayoutParams != null) {
332                 childLayoutParams.right = menuWidth;
333             }
334 
335             itemTop += itemHeight;
336 
337             // Add a horizontal divider (if we need one under this row)
338             if ((mHorizontalDivider != null) && (numHorizDividersRemainingToDraw-- > 0)) {
339                 mHorizontalDividerRects.add(new Rect(0, (int) itemTop, menuWidth,
340                                                      (int) (itemTop + mHorizontalDividerHeight)));
341                 itemTop += mHorizontalDividerHeight;
342             }
343         }
344     }
345 
346     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)347     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
348         if (DBG) log("onMeasure(" + widthMeasureSpec + " x " + heightMeasureSpec + ")...");
349 
350         // Get the desired height of the icon menu view (last row of items does
351         // not have a divider below)
352         final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) * mNumVisibleRows
353                 - mHorizontalDividerHeight;
354 
355         // Maximum possible width and desired height
356         setMeasuredDimension(resolveSize(Integer.MAX_VALUE, widthMeasureSpec),
357                              resolveSize(desiredHeight, heightMeasureSpec));
358 
359         // Position the children
360         positionChildren(mMeasuredWidth, mMeasuredHeight);
361     }
362 
363     @Override
onLayout(boolean changed, int l, int t, int r, int b)364     protected void onLayout(boolean changed, int l, int t, int r, int b) {
365         if (DBG) log("onLayout(changed " + changed
366                      + ", l " + l + " t " + t + " r " + r + " b " + b + ")...");
367 
368         View child;
369         InCallMenuView.LayoutParams childLayoutParams;
370 
371         for (int i = getChildCount() - 1; i >= 0; i--) {
372             child = getChildAt(i);
373             childLayoutParams = (InCallMenuView.LayoutParams) child.getLayoutParams();
374 
375             // Layout children according to positions set during the measure
376             child.layout(childLayoutParams.left, childLayoutParams.top,
377                          childLayoutParams.right, childLayoutParams.bottom);
378         }
379     }
380 
381     @Override
onDraw(Canvas canvas)382     protected void onDraw(Canvas canvas) {
383         if (DBG) log("onDraw()...");
384 
385         if (mHorizontalDivider != null) {
386             // If we have a horizontal divider to draw, draw it at the remembered positions
387             for (int i = mHorizontalDividerRects.size() - 1; i >= 0; i--) {
388                 mHorizontalDivider.setBounds(mHorizontalDividerRects.get(i));
389                 mHorizontalDivider.draw(canvas);
390             }
391         }
392 
393         if (mVerticalDivider != null) {
394             // If we have a vertical divider to draw, draw it at the remembered positions
395             for (int i = mVerticalDividerRects.size() - 1; i >= 0; i--) {
396                 mVerticalDivider.setBounds(mVerticalDividerRects.get(i));
397                 mVerticalDivider.draw(canvas);
398             }
399         }
400     }
401 
402     @Override
dispatchKeyEvent(KeyEvent event)403     public boolean dispatchKeyEvent(KeyEvent event) {
404         if (DBG) log("dispatchKeyEvent(" + event + ")...");
405 
406         // In most other apps, when a menu is up, the menu itself handles
407         // keypresses.  And keys that aren't handled by the menu do NOT
408         // get dispatched to the current Activity.
409         //
410         // But in the in-call UI, we don't have any menu shortcuts, *and*
411         // it's important for buttons like CALL to work normally even
412         // while the menu is up.  So we handle ALL key events (with some
413         // exceptions -- see below) by simply forwarding them to the
414         // InCallScreen.
415 
416         int keyCode = event.getKeyCode();
417         if (event.isDown()) {
418             switch (keyCode) {
419                 // The BACK key dismisses the menu.
420                 case KeyEvent.KEYCODE_BACK:
421                     if (DBG) log("==> BACK key!  handling it ourselves...");
422                     // We don't need to do anything here (since BACK
423                     // is magically handled by the framework); we just
424                     // need to *not* forward it to the InCallScreen.
425                     break;
426 
427                 // Don't send KEYCODE_DPAD_CENTER/KEYCODE_ENTER to the
428                 // InCallScreen either, since the framework needs those to
429                 // activate the focused item when using the trackball.
430                 case KeyEvent.KEYCODE_DPAD_CENTER:
431                 case KeyEvent.KEYCODE_ENTER:
432                     break;
433 
434                 // Anything else gets forwarded to the InCallScreen.
435                 default:
436                     if (DBG) log("==> dispatchKeyEvent: forwarding event to the InCallScreen");
437                     if (mInCallScreen != null) {
438                         return mInCallScreen.onKeyDown(keyCode, event);
439                     }
440                     break;
441             }
442         } else if (mInCallScreen != null &&
443                 (keyCode == KeyEvent.KEYCODE_CALL ||
444                         mInCallScreen.isKeyEventAcceptableDTMF(event))) {
445 
446             // Forward the key-up for the call and dialer buttons to the
447             // InCallScreen.  All other key-up events are NOT handled here,
448             // but instead fall through to dispatchKeyEvent from the superclass.
449             if (DBG) log("==> dispatchKeyEvent: forwarding key up event to the InCallScreen");
450             return mInCallScreen.onKeyUp(keyCode, event);
451         }
452         return super.dispatchKeyEvent(event);
453     }
454 
455 
456     @Override
generateLayoutParams(AttributeSet attrs)457     public LayoutParams generateLayoutParams(AttributeSet attrs) {
458         return new InCallMenuView.LayoutParams(getContext(), attrs);
459     }
460 
461     @Override
checkLayoutParams(ViewGroup.LayoutParams p)462     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
463         // Override to allow type-checking of LayoutParams.
464         return p instanceof InCallMenuView.LayoutParams;
465     }
466 
467     /**
468      * Layout parameters specific to InCallMenuView (stores the left, top,
469      * right, bottom from the measure pass).
470      */
471     public static class LayoutParams extends ViewGroup.MarginLayoutParams {
472         int left, top, right, bottom;
473 
LayoutParams(Context c, AttributeSet attrs)474         public LayoutParams(Context c, AttributeSet attrs) {
475             super(c, attrs);
476         }
477 
LayoutParams(int width, int height)478         public LayoutParams(int width, int height) {
479             super(width, height);
480         }
481     }
482 
log(String msg)483     private void log(String msg) {
484         Log.d(LOG_TAG, msg);
485     }
486 }
487