Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2009 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.launcher3.widget;
     18 
     19 import android.appwidget.AppWidgetHostView;
     20 import android.appwidget.AppWidgetProviderInfo;
     21 import android.content.Context;
     22 import android.content.res.Configuration;
     23 import android.graphics.PointF;
     24 import android.graphics.Rect;
     25 import android.os.Handler;
     26 import android.os.SystemClock;
     27 import android.util.SparseBooleanArray;
     28 import android.view.KeyEvent;
     29 import android.view.LayoutInflater;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.ViewConfiguration;
     33 import android.view.ViewDebug;
     34 import android.view.ViewGroup;
     35 import android.view.accessibility.AccessibilityNodeInfo;
     36 import android.widget.AdapterView;
     37 import android.widget.Advanceable;
     38 import android.widget.RemoteViews;
     39 
     40 import com.android.launcher3.CheckLongPressHelper;
     41 import com.android.launcher3.ItemInfo;
     42 import com.android.launcher3.Launcher;
     43 import com.android.launcher3.LauncherAppWidgetInfo;
     44 import com.android.launcher3.LauncherAppWidgetProviderInfo;
     45 import com.android.launcher3.R;
     46 import com.android.launcher3.SimpleOnStylusPressListener;
     47 import com.android.launcher3.StylusEventHelper;
     48 import com.android.launcher3.Utilities;
     49 import com.android.launcher3.dragndrop.DragLayer;
     50 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
     51 
     52 import java.util.ArrayList;
     53 
     54 /**
     55  * {@inheritDoc}
     56  */
     57 public class LauncherAppWidgetHostView extends AppWidgetHostView
     58         implements TouchCompleteListener, View.OnLongClickListener {
     59 
     60     // Related to the auto-advancing of widgets
     61     private static final long ADVANCE_INTERVAL = 20000;
     62     private static final long ADVANCE_STAGGER = 250;
     63 
     64     // Maintains a list of widget ids which are supposed to be auto advanced.
     65     private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
     66 
     67     protected final LayoutInflater mInflater;
     68 
     69     private final CheckLongPressHelper mLongPressHelper;
     70     private final StylusEventHelper mStylusEventHelper;
     71     protected final Launcher mLauncher;
     72 
     73     @ViewDebug.ExportedProperty(category = "launcher")
     74     private boolean mReinflateOnConfigChange;
     75 
     76     private float mSlop;
     77 
     78     @ViewDebug.ExportedProperty(category = "launcher")
     79     private boolean mChildrenFocused;
     80 
     81     private boolean mIsScrollable;
     82     private boolean mIsAttachedToWindow;
     83     private boolean mIsAutoAdvanceRegistered;
     84     private Runnable mAutoAdvanceRunnable;
     85 
     86     /**
     87      * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
     88      */
     89     private float mScaleToFit = 1f;
     90 
     91     /**
     92      * The translation values to center the widget within its cellspans.
     93      */
     94     private final PointF mTranslationForCentering = new PointF(0, 0);
     95 
     96     public LauncherAppWidgetHostView(Context context) {
     97         super(context);
     98         mLauncher = Launcher.getLauncher(context);
     99         mLongPressHelper = new CheckLongPressHelper(this, this);
    100         mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
    101         mInflater = LayoutInflater.from(context);
    102         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
    103         setBackgroundResource(R.drawable.widget_internal_focus_bg);
    104 
    105         if (Utilities.ATLEAST_OREO) {
    106             setExecutor(Utilities.THREAD_POOL_EXECUTOR);
    107         }
    108     }
    109 
    110     @Override
    111     public boolean onLongClick(View view) {
    112         if (mIsScrollable) {
    113             DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
    114             dragLayer.requestDisallowInterceptTouchEvent(false);
    115         }
    116         view.performLongClick();
    117         return true;
    118     }
    119 
    120     @Override
    121     protected View getErrorView() {
    122         return mInflater.inflate(R.layout.appwidget_error, this, false);
    123     }
    124 
    125     @Override
    126     public void updateAppWidget(RemoteViews remoteViews) {
    127         super.updateAppWidget(remoteViews);
    128 
    129         // The provider info or the views might have changed.
    130         checkIfAutoAdvance();
    131 
    132         // It is possible that widgets can receive updates while launcher is not in the foreground.
    133         // Consequently, the widgets will be inflated for the orientation of the foreground activity
    134         // (framework issue). On resuming, we ensure that any widgets are inflated for the current
    135         // orientation.
    136         mReinflateOnConfigChange = !isSameOrientation();
    137     }
    138 
    139     private boolean isSameOrientation() {
    140         return mLauncher.getResources().getConfiguration().orientation ==
    141                 mLauncher.getOrientation();
    142     }
    143 
    144     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
    145         if (viewGroup instanceof AdapterView) {
    146             return true;
    147         } else {
    148             for (int i=0; i < viewGroup.getChildCount(); i++) {
    149                 View child = viewGroup.getChildAt(i);
    150                 if (child instanceof ViewGroup) {
    151                     if (checkScrollableRecursively((ViewGroup) child)) {
    152                         return true;
    153                     }
    154                 }
    155             }
    156         }
    157         return false;
    158     }
    159 
    160     public boolean onInterceptTouchEvent(MotionEvent ev) {
    161         // Just in case the previous long press hasn't been cleared, we make sure to start fresh
    162         // on touch down.
    163         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    164             mLongPressHelper.cancelLongPress();
    165         }
    166 
    167         // Consume any touch events for ourselves after longpress is triggered
    168         if (mLongPressHelper.hasPerformedLongPress()) {
    169             mLongPressHelper.cancelLongPress();
    170             return true;
    171         }
    172 
    173         // Watch for longpress or stylus button press events at this level to
    174         // make sure users can always pick up this widget
    175         if (mStylusEventHelper.onMotionEvent(ev)) {
    176             mLongPressHelper.cancelLongPress();
    177             return true;
    178         }
    179 
    180         switch (ev.getAction()) {
    181             case MotionEvent.ACTION_DOWN: {
    182                 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
    183 
    184                 if (mIsScrollable) {
    185                      dragLayer.requestDisallowInterceptTouchEvent(true);
    186                 }
    187                 if (!mStylusEventHelper.inStylusButtonPressed()) {
    188                     mLongPressHelper.postCheckForLongPress();
    189                 }
    190                 dragLayer.setTouchCompleteListener(this);
    191                 break;
    192             }
    193 
    194             case MotionEvent.ACTION_UP:
    195             case MotionEvent.ACTION_CANCEL:
    196                 mLongPressHelper.cancelLongPress();
    197                 break;
    198             case MotionEvent.ACTION_MOVE:
    199                 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) {
    200                     mLongPressHelper.cancelLongPress();
    201                 }
    202                 break;
    203         }
    204 
    205         // Otherwise continue letting touch events fall through to children
    206         return false;
    207     }
    208 
    209     public boolean onTouchEvent(MotionEvent ev) {
    210         // If the widget does not handle touch, then cancel
    211         // long press when we release the touch
    212         switch (ev.getAction()) {
    213             case MotionEvent.ACTION_UP:
    214             case MotionEvent.ACTION_CANCEL:
    215                 mLongPressHelper.cancelLongPress();
    216                 break;
    217             case MotionEvent.ACTION_MOVE:
    218                 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) {
    219                     mLongPressHelper.cancelLongPress();
    220                 }
    221                 break;
    222         }
    223         return false;
    224     }
    225 
    226     @Override
    227     protected void onAttachedToWindow() {
    228         super.onAttachedToWindow();
    229         mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    230 
    231         mIsAttachedToWindow = true;
    232         checkIfAutoAdvance();
    233     }
    234 
    235     @Override
    236     protected void onDetachedFromWindow() {
    237         super.onDetachedFromWindow();
    238 
    239         // We can't directly use isAttachedToWindow() here, as this is called before the internal
    240         // state is updated. So isAttachedToWindow() will return true until next frame.
    241         mIsAttachedToWindow = false;
    242         checkIfAutoAdvance();
    243     }
    244 
    245     @Override
    246     public void cancelLongPress() {
    247         super.cancelLongPress();
    248         mLongPressHelper.cancelLongPress();
    249     }
    250 
    251     @Override
    252     public AppWidgetProviderInfo getAppWidgetInfo() {
    253         AppWidgetProviderInfo info = super.getAppWidgetInfo();
    254         if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) {
    255             throw new IllegalStateException("Launcher widget must have"
    256                     + " LauncherAppWidgetProviderInfo");
    257         }
    258         return info;
    259     }
    260 
    261     @Override
    262     public void onTouchComplete() {
    263         if (!mLongPressHelper.hasPerformedLongPress()) {
    264             // If a long press has been performed, we don't want to clear the record of that since
    265             // we still may be receiving a touch up which we want to intercept
    266             mLongPressHelper.cancelLongPress();
    267         }
    268     }
    269 
    270     @Override
    271     public int getDescendantFocusability() {
    272         return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS
    273                 : ViewGroup.FOCUS_BLOCK_DESCENDANTS;
    274     }
    275 
    276     @Override
    277     public boolean dispatchKeyEvent(KeyEvent event) {
    278         if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE
    279                 && event.getAction() == KeyEvent.ACTION_UP) {
    280             mChildrenFocused = false;
    281             requestFocus();
    282             return true;
    283         }
    284         return super.dispatchKeyEvent(event);
    285     }
    286 
    287     @Override
    288     public boolean onKeyDown(int keyCode, KeyEvent event) {
    289         if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
    290             event.startTracking();
    291             return true;
    292         }
    293         return super.onKeyDown(keyCode, event);
    294     }
    295 
    296     @Override
    297     public boolean onKeyUp(int keyCode, KeyEvent event) {
    298         if (event.isTracking()) {
    299             if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
    300                 mChildrenFocused = true;
    301                 ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD);
    302                 focusableChildren.remove(this);
    303                 int childrenCount = focusableChildren.size();
    304                 switch (childrenCount) {
    305                     case 0:
    306                         mChildrenFocused = false;
    307                         break;
    308                     case 1: {
    309                         if (getTag() instanceof ItemInfo) {
    310                             ItemInfo item = (ItemInfo) getTag();
    311                             if (item.spanX == 1 && item.spanY == 1) {
    312                                 focusableChildren.get(0).performClick();
    313                                 mChildrenFocused = false;
    314                                 return true;
    315                             }
    316                         }
    317                         // continue;
    318                     }
    319                     default:
    320                         focusableChildren.get(0).requestFocus();
    321                         return true;
    322                 }
    323             }
    324         }
    325         return super.onKeyUp(keyCode, event);
    326     }
    327 
    328     @Override
    329     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
    330         if (gainFocus) {
    331             mChildrenFocused = false;
    332             dispatchChildFocus(false);
    333         }
    334         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    335     }
    336 
    337     @Override
    338     public void requestChildFocus(View child, View focused) {
    339         super.requestChildFocus(child, focused);
    340         dispatchChildFocus(mChildrenFocused && focused != null);
    341         if (focused != null) {
    342             focused.setFocusableInTouchMode(false);
    343         }
    344     }
    345 
    346     @Override
    347     public void clearChildFocus(View child) {
    348         super.clearChildFocus(child);
    349         dispatchChildFocus(false);
    350     }
    351 
    352     @Override
    353     public boolean dispatchUnhandledMove(View focused, int direction) {
    354         return mChildrenFocused;
    355     }
    356 
    357     private void dispatchChildFocus(boolean childIsFocused) {
    358         // The host view's background changes when selected, to indicate the focus is inside.
    359         setSelected(childIsFocused);
    360     }
    361 
    362     public void switchToErrorView() {
    363         // Update the widget with 0 Layout id, to reset the view to error view.
    364         updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
    365     }
    366 
    367     @Override
    368     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    369         try {
    370             super.onLayout(changed, left, top, right, bottom);
    371         } catch (final RuntimeException e) {
    372             post(new Runnable() {
    373                 @Override
    374                 public void run() {
    375                     switchToErrorView();
    376                 }
    377             });
    378         }
    379 
    380         mIsScrollable = checkScrollableRecursively(this);
    381     }
    382 
    383     @Override
    384     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    385         super.onInitializeAccessibilityNodeInfo(info);
    386         info.setClassName(getClass().getName());
    387     }
    388 
    389     @Override
    390     protected void onWindowVisibilityChanged(int visibility) {
    391         super.onWindowVisibilityChanged(visibility);
    392         maybeRegisterAutoAdvance();
    393     }
    394 
    395     private void checkIfAutoAdvance() {
    396         boolean isAutoAdvance = false;
    397         Advanceable target = getAdvanceable();
    398         if (target != null) {
    399             isAutoAdvance = true;
    400             target.fyiWillBeAdvancedByHostKThx();
    401         }
    402 
    403         boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0;
    404         if (isAutoAdvance != wasAutoAdvance) {
    405             if (isAutoAdvance) {
    406                 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true);
    407             } else {
    408                 sAutoAdvanceWidgetIds.delete(getAppWidgetId());
    409             }
    410             maybeRegisterAutoAdvance();
    411         }
    412     }
    413 
    414     private Advanceable getAdvanceable() {
    415         AppWidgetProviderInfo info = getAppWidgetInfo();
    416         if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) {
    417             return null;
    418         }
    419         View v = findViewById(info.autoAdvanceViewId);
    420         return (v instanceof Advanceable) ? (Advanceable) v : null;
    421     }
    422 
    423     private void maybeRegisterAutoAdvance() {
    424         Handler handler = getHandler();
    425         boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null
    426                 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0);
    427         if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) {
    428             mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance;
    429             if (mAutoAdvanceRunnable == null) {
    430                 mAutoAdvanceRunnable = new Runnable() {
    431                     @Override
    432                     public void run() {
    433                         runAutoAdvance();
    434                     }
    435                 };
    436             }
    437 
    438             handler.removeCallbacks(mAutoAdvanceRunnable);
    439             scheduleNextAdvance();
    440         }
    441     }
    442 
    443     private void scheduleNextAdvance() {
    444         if (!mIsAutoAdvanceRegistered) {
    445             return;
    446         }
    447         long now = SystemClock.uptimeMillis();
    448         long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) +
    449                 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId());
    450         Handler handler = getHandler();
    451         if (handler != null) {
    452             handler.postAtTime(mAutoAdvanceRunnable, advanceTime);
    453         }
    454     }
    455 
    456     private void runAutoAdvance() {
    457         Advanceable target = getAdvanceable();
    458         if (target != null) {
    459             target.advance();
    460         }
    461         scheduleNextAdvance();
    462     }
    463 
    464     public void setScaleToFit(float scale) {
    465         mScaleToFit = scale;
    466         setScaleX(scale);
    467         setScaleY(scale);
    468     }
    469 
    470     public float getScaleToFit() {
    471         return mScaleToFit;
    472     }
    473 
    474     public void setTranslationForCentering(float x, float y) {
    475         mTranslationForCentering.set(x, y);
    476         setTranslationX(x);
    477         setTranslationY(y);
    478     }
    479 
    480     public PointF getTranslationForCentering() {
    481         return mTranslationForCentering;
    482     }
    483 
    484     @Override
    485     protected void onConfigurationChanged(Configuration newConfig) {
    486         super.onConfigurationChanged(newConfig);
    487 
    488         // Only reinflate when the final configuration is same as the required configuration
    489         if (mReinflateOnConfigChange && isSameOrientation()) {
    490             mReinflateOnConfigChange = false;
    491             reInflate();
    492         }
    493     }
    494 
    495     public void reInflate() {
    496         if (!isAttachedToWindow()) {
    497             return;
    498         }
    499         LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
    500         // Remove and rebind the current widget (which was inflated in the wrong
    501         // orientation), but don't delete it from the database
    502         mLauncher.removeItem(this, info, false  /* deleteFromDb */);
    503         mLauncher.bindAppWidget(info);
    504     }
    505 }
    506