summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher2/Search.java
blob: 8a7c3529c9d62eb1c39d650401b761d72968c10f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.launcher;

import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.server.search.SearchableInfo;
import android.server.search.Searchables;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnLongClickListener;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;

public class Search extends LinearLayout 
        implements OnClickListener, OnKeyListener, OnLongClickListener {

    // Speed at which the widget slides up/down, in pixels/ms.
    private static final float ANIMATION_VELOCITY = 1.0f;

    private final String TAG = "SearchWidget";

    private Launcher mLauncher;

    private TextView mSearchText;
    private ImageButton mVoiceButton;

    /** The animation that morphs the search widget to the search dialog. */
    private Animation mMorphAnimation;

    /** The animation that morphs the search widget back to its normal position. */
    private Animation mUnmorphAnimation;

    // These four are passed to Launcher.startSearch() when the search widget
    // has finished morphing. They are instance variables to make it possible to update
    // them while the widget is morphing.
    private String mInitialQuery;
    private boolean mSelectInitialQuery;    
    private Bundle mAppSearchData;
    private boolean mGlobalSearch;

    // For voice searching
    private Intent mVoiceSearchIntent;
    
    private Drawable mGooglePlaceholder;
    
    private SearchManager mSearchManager;

    /**
     * Used to inflate the Workspace from XML.
     *
     * @param context The application's context.
     * @param attrs The attributes set containing the Workspace's customization values.
     */
    public Search(Context context, AttributeSet attrs) {
        super(context, attrs);

        Interpolator interpolator = new AccelerateDecelerateInterpolator();

        mMorphAnimation = new ToParentOriginAnimation();
        // no need to apply transformation before the animation starts,
        // since the gadget is already in its normal place.
        mMorphAnimation.setFillBefore(false);
        // stay in the top position after the animation finishes
        mMorphAnimation.setFillAfter(true);
        mMorphAnimation.setInterpolator(interpolator);
        mMorphAnimation.setAnimationListener(new Animation.AnimationListener() {
            // The amount of time before the animation ends to show the search dialog.
            private static final long TIME_BEFORE_ANIMATION_END = 80;
            
            // The runnable which we'll pass to our handler to show the search dialog.
            private final Runnable mShowSearchDialogRunnable = new Runnable() {
                public void run() {
                    showSearchDialog();
                }
            };
            
            public void onAnimationEnd(Animation animation) { }
            public void onAnimationRepeat(Animation animation) { }
            public void onAnimationStart(Animation animation) {
                // Make the search dialog show up ideally *just* as the animation reaches
                // the top, to aid the illusion that the widget becomes the search dialog.
                // Otherwise, there is a short delay when the widget reaches the top before
                // the search dialog shows. We do this roughly 80ms before the animation ends.
                getHandler().postDelayed(
                        mShowSearchDialogRunnable,
                        Math.max(mMorphAnimation.getDuration() - TIME_BEFORE_ANIMATION_END, 0));
            }
        });

        mUnmorphAnimation = new FromParentOriginAnimation();
        // stay in the top position until the animation starts
        mUnmorphAnimation.setFillBefore(true);
        // no need to apply transformation after the animation finishes,
        // since the gadget is now back in its normal place.
        mUnmorphAnimation.setFillAfter(false);
        mUnmorphAnimation.setInterpolator(interpolator);
        mUnmorphAnimation.setAnimationListener(new Animation.AnimationListener(){
            public void onAnimationEnd(Animation animation) {
                clearAnimation();
            }
            public void onAnimationRepeat(Animation animation) { }
            public void onAnimationStart(Animation animation) { }
        });
        
        mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
        mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL,
                android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
        
        mSearchManager = (SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE);
    }

    /**
     * Implements OnClickListener.
     */
    public void onClick(View v) {
        if (v == mVoiceButton) {
            startVoiceSearch();
        } else {
            mLauncher.onSearchRequested();
        }
    }

    private void startVoiceSearch() {
        try {
            getContext().startActivity(mVoiceSearchIntent);
        } catch (ActivityNotFoundException ex) {
            // Should not happen, since we check the availability of
            // voice search before showing the button. But just in case...
            Log.w(TAG, "Could not find voice search activity");
        }
    }

    /**
     * Sets the query text. The query field is not editable, instead we forward
     * the key events to the launcher, which keeps track of the text, 
     * calls setQuery() to show it, and gives it to the search dialog.
     */
    public void setQuery(String query) {
        mSearchText.setText(query, TextView.BufferType.NORMAL);
    }

    /**
     * Morph the search gadget to the search dialog.
     * See {@link Activity.startSearch()} for the arguments.
     */
    public void startSearch(String initialQuery, boolean selectInitialQuery, 
            Bundle appSearchData, boolean globalSearch) {
        mInitialQuery = initialQuery;
        mSelectInitialQuery = selectInitialQuery;
        mAppSearchData = appSearchData;
        mGlobalSearch = globalSearch;
        
        if (isAtTop()) {
            showSearchDialog();
        } else {
            // Call up the keyboard before we actually call the search dialog so that it
            // (hopefully) animates in at about the same time as the widget animation, and
            // so that it becomes available as soon as possible. Only do this if a hard
            // keyboard is not currently available.
            if (getContext().getResources().getConfiguration().hardKeyboardHidden ==
                    Configuration.HARDKEYBOARDHIDDEN_YES) {
                InputMethodManager inputManager = (InputMethodManager)
                        getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                inputManager.showSoftInputUnchecked(0, null);
            }
            
            // Start the animation, unless it has already started.
            if (getAnimation() != mMorphAnimation) {
                mMorphAnimation.setDuration(getAnimationDuration());
                startAnimation(mMorphAnimation);
            }
        }
    }

    /**
     * Shows the system search dialog immediately, without any animation.
     */
    private void showSearchDialog() {
        mLauncher.showSearchDialog(
                mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch);
    }

    /**
     * Restore the search gadget to its normal position.
     * 
     * @param animate Whether to animate the movement of the gadget.
     */
    public void stopSearch(boolean animate) {
        setQuery("");
        
        // Only restore if we are not already restored.
        if (getAnimation() == mMorphAnimation) {
            if (animate && !isAtTop()) {
                mUnmorphAnimation.setDuration(getAnimationDuration());
                startAnimation(mUnmorphAnimation);
            } else {
                clearAnimation();
            }
        }
    }

    private boolean isAtTop() {
        return getTop() == 0;
    }

    private int getAnimationDuration() {
        return (int) (getTop() / ANIMATION_VELOCITY);
    }

    /**
     * Modify clearAnimation() to invalidate the parent. This works around
     * an issue where the region where the end of the animation placed the view
     * was not redrawn after clearing the animation.
     */
    @Override
    public void clearAnimation() {
        Animation animation = getAnimation();
        if (animation != null) {
            super.clearAnimation();
            if (animation.hasEnded() 
                    && animation.getFillAfter()
                    && animation.willChangeBounds()) {
                ((View) getParent()).invalidate();
            } else {
                invalidate();
            }
        }
    }
    
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (!event.isSystem() && 
                (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
            // Forward key events to Launcher, which will forward text 
            // to search dialog
            switch (event.getAction()) {
                case KeyEvent.ACTION_DOWN:
                    return mLauncher.onKeyDown(keyCode, event);
                case KeyEvent.ACTION_MULTIPLE:
                    return mLauncher.onKeyMultiple(keyCode, event.getRepeatCount(), event);
                case KeyEvent.ACTION_UP:
                    return mLauncher.onKeyUp(keyCode, event);
            }
        }
        return false;
    }

    /**
     * Implements OnLongClickListener to pass long clicks on child views 
     * to the widget. This makes it possible to pick up the widget by long
     * clicking on the text field or a button.
     */
    public boolean onLongClick(View v) {
        return performLongClick();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mSearchText = (TextView) findViewById(R.id.search_src_text);
        mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn);
        
        mGooglePlaceholder = getContext().getResources().getDrawable(R.drawable.placeholder_google);
        mContext.registerReceiver(mBroadcastReceiver,
                new IntentFilter(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED));

        mSearchText.setOnKeyListener(this);

        mSearchText.setOnClickListener(this);
        mVoiceButton.setOnClickListener(this);
        setOnClickListener(this);        

        mSearchText.setOnLongClickListener(this);
        mVoiceButton.setOnLongClickListener(this);

        configureVoiceSearchButton();
        setUpTextField();
    }
    
    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mBroadcastReceiver != null) getContext().unregisterReceiver(mBroadcastReceiver);
    }

    /**
     * If appropriate & available, configure voice search
     * 
     * Note:  Because the home screen search widget is always web search, we only check for
     * getVoiceSearchLaunchWebSearch() modes.  We don't support the alternate form of app-specific
     * voice search.
     */
    private void configureVoiceSearchButton() {
        // Enable the voice search button if there is an activity that can handle it
        PackageManager pm = getContext().getPackageManager();
        ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent,
                PackageManager.MATCH_DEFAULT_ONLY);
        boolean voiceSearchVisible = ri != null;

        // finally, set visible state of voice search button, as appropriate
        mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE);
    }
    
    /**
     * Sets up the look of the text field. If Google is the chosen search provider, includes
     * a Google logo as placeholder.
     */
    private void setUpTextField() {
        boolean showGooglePlaceholder = false;
        SearchableInfo webSearchSearchable = mSearchManager.getDefaultSearchableForWebSearch();
        if (webSearchSearchable != null) {
            ComponentName webSearchComponent = webSearchSearchable.getSearchActivity();
            if (webSearchComponent != null) {
                String componentString = webSearchComponent.flattenToShortString();
                if (Searchables.ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME.equals(componentString) ||
                        Searchables.GOOGLE_SEARCH_COMPONENT_NAME.equals(componentString)) {
                    showGooglePlaceholder = true;
                }
            }
        }
        
        mSearchText.setCompoundDrawablesWithIntrinsicBounds(
                showGooglePlaceholder ? mGooglePlaceholder : null, null, null, null);
    }

    /**
     * Sets the {@link Launcher} that this gadget will call on to display the search dialog. 
     */
    public void setLauncher(Launcher launcher) {
        mLauncher = launcher;
    }
        
    // Broadcast receiver for web search provider change notifications
    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED.equals(action)) {
                setUpTextField();
            }
        }
    };

    /** 
     * Moves the view to the top left corner of its parent.
     */
    private class ToParentOriginAnimation extends Animation {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            float dx = -getLeft() * interpolatedTime;
            float dy = -getTop() * interpolatedTime;
            t.getMatrix().setTranslate(dx, dy);
        }
    }

    /** 
     * Moves the view from the top left corner of its parent.
     */
    private class FromParentOriginAnimation extends Animation {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            float dx = -getLeft() * (1.0f - interpolatedTime);
            float dy = -getTop() * (1.0f - interpolatedTime);
            t.getMatrix().setTranslate(dx, dy);
        }
    }

}