Home | History | Annotate | Download | only in phone
      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 
     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      */
    165     void clearInCallScreenReference() {
    166         mInCallScreen = null;
    167     }
    168 
    169     /**
    170      * Adds an InCallMenuItemView to the specified row.
    171      */
    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      */
    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 
    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      */
    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
    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
    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
    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
    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
    457     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    458         return new InCallMenuView.LayoutParams(getContext(), attrs);
    459     }
    460 
    461     @Override
    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 
    474         public LayoutParams(Context c, AttributeSet attrs) {
    475             super(c, attrs);
    476         }
    477 
    478         public LayoutParams(int width, int height) {
    479             super(width, height);
    480         }
    481     }
    482 
    483     private void log(String msg) {
    484         Log.d(LOG_TAG, msg);
    485     }
    486 }
    487