OSDN Git Service

CM File Manager : Populate results as they are found while searching
[android-x86/packages-apps-CMFileManager.git] / src / com / cyanogenmod / filemanager / activities / SearchActivity.java
1 /*
2  * Copyright (C) 2012 The CyanogenMod 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.cyanogenmod.filemanager.activities;
18
19 import android.app.ActionBar;
20 import android.app.Activity;
21 import android.app.AlertDialog;
22 import android.app.SearchManager;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.res.Configuration;
29 import android.graphics.Color;
30 import android.graphics.PorterDuff;
31 import android.os.Bundle;
32 import android.os.Parcelable;
33 import android.preference.PreferenceActivity;
34 import android.provider.SearchRecentSuggestions;
35 import android.text.Html;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.view.KeyEvent;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.widget.AdapterView;
42 import android.widget.AdapterView.OnItemClickListener;
43 import android.widget.AdapterView.OnItemLongClickListener;
44
45 import android.widget.ArrayAdapter;
46 import android.widget.ImageView;
47 import android.widget.ListView;
48 import android.widget.ProgressBar;
49 import android.widget.Spinner;
50 import android.widget.TextView;
51 import android.widget.Toast;
52 import com.cyanogenmod.filemanager.FileManagerApplication;
53 import com.cyanogenmod.filemanager.R;
54 import com.cyanogenmod.filemanager.activities.preferences.SearchPreferenceFragment;
55 import com.cyanogenmod.filemanager.activities.preferences.SettingsPreferences;
56 import com.cyanogenmod.filemanager.adapters.SearchResultAdapter;
57 import com.cyanogenmod.filemanager.commands.AsyncResultExecutable;
58 import com.cyanogenmod.filemanager.commands.ConcurrentAsyncResultListener;
59 import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory;
60 import com.cyanogenmod.filemanager.console.RelaunchableException;
61 import com.cyanogenmod.filemanager.listeners.OnRequestRefreshListener;
62 import com.cyanogenmod.filemanager.model.Directory;
63 import com.cyanogenmod.filemanager.model.FileSystemObject;
64 import com.cyanogenmod.filemanager.model.ParentDirectory;
65 import com.cyanogenmod.filemanager.model.Query;
66 import com.cyanogenmod.filemanager.model.SearchResult;
67 import com.cyanogenmod.filemanager.model.Symlink;
68 import com.cyanogenmod.filemanager.parcelables.SearchInfoParcelable;
69 import com.cyanogenmod.filemanager.preferences.AccessMode;
70 import com.cyanogenmod.filemanager.preferences.FileManagerSettings;
71 import com.cyanogenmod.filemanager.preferences.Preferences;
72 import com.cyanogenmod.filemanager.providers.RecentSearchesContentProvider;
73 import com.cyanogenmod.filemanager.ui.ThemeManager;
74 import com.cyanogenmod.filemanager.ui.ThemeManager.Theme;
75 import com.cyanogenmod.filemanager.ui.dialogs.ActionsDialog;
76 import com.cyanogenmod.filemanager.ui.policy.DeleteActionPolicy;
77 import com.cyanogenmod.filemanager.ui.policy.IntentsActionPolicy;
78 import com.cyanogenmod.filemanager.ui.widgets.ButtonItem;
79 import com.cyanogenmod.filemanager.ui.widgets.FlingerListView;
80 import com.cyanogenmod.filemanager.ui.widgets.FlingerListView.OnItemFlingerListener;
81 import com.cyanogenmod.filemanager.ui.widgets.FlingerListView.OnItemFlingerResponder;
82 import com.cyanogenmod.filemanager.util.CommandHelper;
83 import com.cyanogenmod.filemanager.util.DialogHelper;
84 import com.cyanogenmod.filemanager.util.ExceptionUtil;
85 import com.cyanogenmod.filemanager.util.ExceptionUtil.OnRelaunchCommandResult;
86 import com.cyanogenmod.filemanager.util.FileHelper;
87 import com.cyanogenmod.filemanager.util.MimeTypeHelper;
88 import com.cyanogenmod.filemanager.util.MimeTypeHelper.MimeTypeCategory;
89 import com.cyanogenmod.filemanager.util.SearchHelper;
90 import com.cyanogenmod.filemanager.util.StorageHelper;
91
92 import java.io.FileNotFoundException;
93 import java.io.Serializable;
94 import java.util.ArrayList;
95 import java.util.HashSet;
96 import java.util.List;
97
98 /**
99  * An activity for search files and folders.
100  */
101 public class SearchActivity extends Activity
102     implements OnItemClickListener, OnItemLongClickListener, OnRequestRefreshListener,
103         AdapterView.OnItemSelectedListener {
104
105     private static final String TAG = "SearchActivity"; //$NON-NLS-1$
106
107     private static boolean DEBUG = false;
108
109     /**
110      * An {@link Intent} action for restore view information.
111      */
112     public static final String ACTION_RESTORE =
113             "com.cyanogenmod.filemanager.activities.SearchActivity#Restore"; //$NON-NLS-1$
114
115     /**
116      * Intent extra parameter for search in the selected directory on enter.
117      */
118     public static final String EXTRA_SEARCH_DIRECTORY = "extra_search_directory";  //$NON-NLS-1$
119
120     /**
121      * Intent extra parameter for pass the restore information.
122      */
123     public static final String EXTRA_SEARCH_RESTORE = "extra_search_restore";  //$NON-NLS-1$
124
125     /**
126      * Intent extra parameter for passing the target mime type information.
127      */
128     public static final String EXTRA_SEARCH_MIMETYPE = "extra_search_mimetype";  //$NON-NLS-1$
129
130     //Minimum characters to allow query
131     private static final int MIN_CHARS_SEARCH = 3;
132
133     private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() {
134         @Override
135         public void onReceive(Context context, Intent intent) {
136             if (intent != null) {
137                 if (intent.getAction().compareTo(
138                         FileManagerSettings.INTENT_SETTING_CHANGED) == 0) {
139
140                     // The settings has changed
141                     String key = intent.getStringExtra(
142                             FileManagerSettings.EXTRA_SETTING_CHANGED_KEY);
143                     if (key != null) {
144                         if (SearchActivity.this.mSearchListView.getAdapter() != null &&
145                            (key.compareTo(
146                                    FileManagerSettings.
147                                        SETTINGS_HIGHLIGHT_TERMS.getId()) == 0 ||
148                             key.compareTo(
149                                     FileManagerSettings.
150                                         SETTINGS_SHOW_RELEVANCE_WIDGET.getId()) == 0 ||
151                             key.compareTo(
152                                     FileManagerSettings.
153                                         SETTINGS_SORT_SEARCH_RESULTS_MODE.getId()) == 0)) {
154
155                             // Recreate the adapter
156                             int pos = SearchActivity.
157                                         this.mSearchListView.getFirstVisiblePosition();
158                             mAdapter.notifyDataSetChanged();
159                             SearchActivity.this.mSearchListView.setSelection(pos);
160                             return;
161                         }
162                     }
163                 } else if (intent.getAction().compareTo(
164                         FileManagerSettings.INTENT_THEME_CHANGED) == 0) {
165                     applyTheme();
166                 }
167             }
168         }
169     };
170
171     /**
172      * A listener for flinging events from {@link FlingerListView}
173      */
174     private final OnItemFlingerListener mOnItemFlingerListener = new OnItemFlingerListener() {
175
176         @Override
177         public boolean onItemFlingerStart(
178                 AdapterView<?> parent, View view, int position, long id) {
179             try {
180                 // Response if the item can be removed
181                 SearchResultAdapter adapter = (SearchResultAdapter)parent.getAdapter();
182                 SearchResult result = adapter.getItem(position);
183                 if (result != null && result.getFso() != null) {
184                     if (result.getFso() instanceof ParentDirectory) {
185                         // This is not possible ...
186                         return false;
187                     }
188                     return true;
189                 }
190             } catch (Exception e) {
191                 ExceptionUtil.translateException(SearchActivity.this, e, true, false);
192             }
193             return false;
194         }
195
196         @Override
197         public void onItemFlingerEnd(OnItemFlingerResponder responder,
198                 AdapterView<?> parent, View view, int position, long id) {
199
200             try {
201                 // Response if the item can be removed
202                 SearchResultAdapter adapter = (SearchResultAdapter)parent.getAdapter();
203                 SearchResult result = adapter.getItem(position);
204                 if (result != null && result.getFso() != null) {
205                     DeleteActionPolicy.removeFileSystemObject(
206                             SearchActivity.this,
207                             result.getFso(),
208                             null,
209                             SearchActivity.this,
210                             responder);
211                     return;
212                 }
213
214                 // Cancels the flinger operation
215                 responder.cancel();
216
217             } catch (Exception e) {
218                 ExceptionUtil.translateException(SearchActivity.this, e, true, false);
219                 responder.cancel();
220             }
221         }
222     };
223
224     private ConcurrentAsyncResultListener mAsyncListener = new ConcurrentAsyncResultListener() {
225         /**
226          * {@inheritDoc}
227          */
228         @Override
229         public void onConcurrentAsyncStart() {
230             mSearchInProgress = true;
231             mAdapter.startStreaming();
232         }
233
234         /**
235          * {@inheritDoc}
236          */
237         @Override
238         public void onConcurrentAsyncEnd(boolean cancelled) {
239             mSearchInProgress = false;
240             mSearchListView.post(new Runnable() {
241                 @Override
242                 public void run() {
243                     try {
244                         mAdapter.stopStreaming();
245                         int resultsSize = mAdapter.resultsSize();
246                         mStreamingSearchProgress.setVisibility(View.INVISIBLE);
247                         if (mMimeTypeCategories.size() > 1) {
248                             mMimeTypeSpinner.setVisibility(View.VISIBLE);
249                         }
250                         mSearchListView.setVisibility(resultsSize > 0 ? View.VISIBLE : View.GONE);
251                         mEmptyListMsg.setVisibility(resultsSize > 0 ? View.GONE : View.VISIBLE);
252
253                     } catch (Throwable ex) {
254                         Log.e(TAG, "onAsyncEnd method fails", ex); //$NON-NLS-1$
255                     }
256                 }
257             });
258         }
259
260         /**
261          * {@inheritDoc}
262          */
263         @Override
264         @SuppressWarnings("unchecked")
265         public void onConcurrentPartialResult(final Object partialResults) {
266             //Saved in the global result list, for save at the end
267             FileSystemObject result = null;
268             if (partialResults instanceof FileSystemObject) {
269                 FileSystemObject fso = (FileSystemObject) partialResults;
270                 if (mMimeTypeCategories == null || mMimeTypeCategories.contains(MimeTypeHelper
271                         .getCategory(SearchActivity.this, fso))) {
272                     SearchActivity.this.mResultList.add((FileSystemObject) partialResults);
273                     showSearchResult((FileSystemObject) partialResults);
274                 }
275             } else {
276                 List<FileSystemObject> fsoList = (List<FileSystemObject>) partialResults;
277                 for (FileSystemObject fso : fsoList) {
278                     if (mMimeTypeCategories == null || mMimeTypeCategories.contains(MimeTypeHelper
279                             .getCategory(SearchActivity.this, fso))) {
280                         SearchActivity.this.mResultList.add(fso);
281                         showSearchResult(fso);
282                     }
283                 }
284             }
285
286             //Notify progress
287             mSearchListView.post(new Runnable() {
288                 @Override
289                 public void run() {
290                     int progress = mAdapter.resultsSize();
291                     String foundItems =
292                             getResources().
293                                     getQuantityString(
294                                             R.plurals.search_found_items, progress,
295                                             Integer.valueOf(progress) );
296                     mSearchFoundItems.setText(
297                             getString(
298                                     R.string.search_found_items_in_directory,
299                                     foundItems,
300                                     mSearchDirectory));
301                 }
302             });
303         }
304
305         /**
306          * {@inheritDoc}
307          */
308         @Override
309         public void onConcurrentAsyncExitCode(int exitCode) {/**NON BLOCK**/}
310
311         /**
312          * {@inheritDoc}
313          */
314         @Override
315         public void onConcurrentException(Exception cause) {
316             //Capture the exception
317             ExceptionUtil.translateException(SearchActivity.this, cause);
318         }
319     };
320
321     /**
322      * @hide
323      */
324     AsyncResultExecutable mExecutable = null;
325
326     /**
327      * @hide
328      */
329     ListView mSearchListView;
330     /**
331      * @hide
332      */
333     ProgressBar mSearchWaiting;
334     /**
335      * @hide
336      */
337     TextView mSearchFoundItems;
338     /**
339      * @hide
340      */
341     TextView mSearchTerms;
342     private View mEmptyListMsg;
343
344     /**
345      * @hide
346      */
347     Spinner mMimeTypeSpinner;
348
349     private String mSearchDirectory;
350     /**
351      * @hide
352      */
353     List<FileSystemObject> mResultList;
354     /**
355      * @hide
356      */
357     Query mQuery;
358
359     /**
360      * @hide
361      */
362     SearchInfoParcelable mRestoreState;
363
364     /**
365      * @hide
366      */
367     boolean mChRooted;
368
369     /**
370      * @hide
371      */
372     HashSet<MimeTypeCategory> mMimeTypeCategories;
373
374     private SearchResultAdapter mAdapter;
375     private ProgressBar mStreamingSearchProgress;
376     private boolean mSearchInProgress;
377
378     /**
379      * {@inheritDoc}
380      */
381     @Override
382     protected void onCreate(Bundle state) {
383         if (DEBUG) {
384             Log.d(TAG, "SearchActivity.onCreate"); //$NON-NLS-1$
385         }
386
387         // Check if app is running in chrooted mode
388         this.mChRooted = FileManagerApplication.getAccessMode().compareTo(AccessMode.SAFE) == 0;
389
390         // Register the broadcast receiver
391         IntentFilter filter = new IntentFilter();
392         filter.addAction(FileManagerSettings.INTENT_SETTING_CHANGED);
393         filter.addAction(FileManagerSettings.INTENT_THEME_CHANGED);
394         registerReceiver(this.mNotificationReceiver, filter);
395
396         // Set the theme before setContentView
397         Theme theme = ThemeManager.getCurrentTheme(this);
398         theme.setBaseTheme(this, false);
399
400         //Set in transition
401         overridePendingTransition(R.anim.translate_to_right_in, R.anim.hold_out);
402
403         //Set the main layout of the activity
404         setContentView(R.layout.search);
405
406         //Restore state
407         if (state != null) {
408             restoreState(state);
409         }
410
411         //Initialize action bars and searc
412         initTitleActionBar();
413         initComponents();
414
415         // Apply current theme
416         applyTheme();
417
418         if (this.mRestoreState != null) {
419             //Restore activity from cached data
420             loadFromCacheData();
421         } else {
422             //New query
423             if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
424                 initSearch();
425             } else if (ACTION_RESTORE.equals(getIntent().getAction())) {
426                 restoreState(getIntent().getExtras());
427                 loadFromCacheData();
428             }
429         }
430
431         //Save state
432         super.onCreate(state);
433     }
434
435     /**
436      * {@inheritDoc}
437      */
438     @Override
439     protected void onDestroy() {
440         if (DEBUG) {
441             Log.d(TAG, "SearchActivity.onDestroy"); //$NON-NLS-1$
442         }
443
444         // Unregister the receiver
445         try {
446             unregisterReceiver(this.mNotificationReceiver);
447         } catch (Throwable ex) {
448             /**NON BLOCK**/
449         }
450
451         //All destroy. Continue
452         super.onDestroy();
453     }
454
455     /**
456      * {@inheritDoc}
457      */
458     @Override
459     public void onConfigurationChanged(Configuration newConfig) {
460         super.onConfigurationChanged(newConfig);
461     }
462
463     /**
464      * {@inheritDoc}
465      */
466     @Override
467     protected void onNewIntent(Intent intent) {
468         //New query
469         if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
470             initSearch();
471         }
472     }
473
474     /**
475      * {@inheritDoc}
476      */
477     @Override
478     protected void onPause() {
479         //Set out transition
480         overridePendingTransition(R.anim.hold_in, R.anim.translate_to_left_out);
481         super.onPause();
482     }
483
484     /**
485      * {@inheritDoc}
486      */
487     @Override
488     protected void onSaveInstanceState(Bundle outState) {
489         if (DEBUG) {
490             Log.d(TAG, "SearchActivity.onSaveInstanceState"); //$NON-NLS-1$
491         }
492         saveState(outState);
493         super.onSaveInstanceState(outState);
494     }
495
496     /**
497      * Method that save the instance of the activity.
498      *
499      * @param state The current state of the activity
500      */
501     private void saveState(Bundle state) {
502         try {
503             if (this.mSearchListView.getAdapter() != null) {
504                 state.putParcelable(EXTRA_SEARCH_RESTORE, createSearchInfo());
505             }
506         } catch (Throwable ex) {
507             Log.w(TAG, "The state can't be saved", ex); //$NON-NLS-1$
508         }
509     }
510
511     /**
512      * Method that restore the instance of the activity.
513      *
514      * @param state The previous state of the activity
515      */
516     private void restoreState(Bundle state) {
517         try {
518             if (state.containsKey(EXTRA_SEARCH_RESTORE)) {
519                 this.mRestoreState = state.getParcelable(EXTRA_SEARCH_RESTORE);
520             }
521         } catch (Throwable ex) {
522             Log.w(TAG, "The state can't be restored", ex); //$NON-NLS-1$
523         }
524     }
525
526     /**
527      * Method that initializes the titlebar of the activity.
528      */
529     private void initTitleActionBar() {
530         //Configure the action bar options
531         // Set up the action bar to show a dropdown list.
532         final ActionBar actionBar = getActionBar();
533         actionBar.setDisplayShowTitleEnabled(false);
534         actionBar.setBackgroundDrawable(
535                 getResources().getDrawable(R.drawable.bg_material_titlebar));
536         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
537         actionBar.setDisplayHomeAsUpEnabled(true);
538
539         View customTitle = getLayoutInflater().inflate(R.layout.simple_customtitle, null, false);
540
541         TextView title = (TextView)customTitle.findViewById(R.id.customtitle_title);
542         title.setText(R.string.search);
543         title.setContentDescription(getString(R.string.search));
544         ButtonItem configuration = (ButtonItem)customTitle.findViewById(R.id.ab_button1);
545         configuration.setImageResource(R.drawable.ic_material_light_config);
546         configuration.setVisibility(View.VISIBLE);
547         actionBar.setCustomView(customTitle);
548     }
549
550     /**
551      * Method that initializes the component of the activity.
552      */
553     private void initComponents() {
554         //Empty list view
555         this.mEmptyListMsg = findViewById(R.id.search_empty_msg);
556         //The list view
557         this.mSearchListView = (ListView)findViewById(R.id.search_listview);
558         this.mSearchListView.setOnItemClickListener(this);
559         this.mSearchListView.setOnItemLongClickListener(this);
560
561         // If we should set the listview to response to flinger gesture detection
562         boolean useFlinger =
563                 Preferences.getSharedPreferences().getBoolean(
564                         FileManagerSettings.SETTINGS_USE_FLINGER.getId(),
565                             ((Boolean)FileManagerSettings.
566                                     SETTINGS_USE_FLINGER.
567                                         getDefaultValue()).booleanValue());
568         if (useFlinger) {
569             ((FlingerListView)this.mSearchListView).
570                     setOnItemFlingerListener(this.mOnItemFlingerListener);
571         }
572
573         //Other components
574         this.mSearchWaiting = (ProgressBar)findViewById(R.id.search_waiting);
575         mStreamingSearchProgress = (ProgressBar) findViewById(R.id.streaming_progress_bar);
576         mStreamingSearchProgress.getIndeterminateDrawable()
577                 .setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
578
579         this.mSearchFoundItems = (TextView)findViewById(R.id.search_status_found_items);
580         setFoundItems(0, ""); //$NON-NLS-1$
581         this.mSearchTerms = (TextView)findViewById(R.id.search_status_query_terms);
582         this.mSearchTerms.setText(
583                 Html.fromHtml(getString(R.string.search_terms, ""))); //$NON-NLS-1$
584
585         // populate Mime Types spinner for search results filtering
586         mMimeTypeSpinner = (Spinner) findViewById(R.id.search_status_type_spinner);
587
588         ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
589                 R.layout.search_spinner_item,
590                 MimeTypeHelper.MimeTypeCategory.getFriendlyLocalizedNames(this));
591         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
592         mMimeTypeSpinner.setAdapter(adapter);
593         mMimeTypeSpinner.setOnItemSelectedListener(this);
594     }
595
596     /**
597      * Method invoked when an action item is clicked.
598      *
599      * @param view The button pushed
600      */
601     public void onActionBarItemClick(View view) {
602         switch (view.getId()) {
603             case R.id.ab_button1:
604                 //Settings
605                 Intent settings = new Intent(this, SettingsPreferences.class);
606                 settings.putExtra(
607                         PreferenceActivity.EXTRA_SHOW_FRAGMENT,
608                         SearchPreferenceFragment.class.getName());
609                 startActivity(settings);
610                 break;
611
612             default:
613                 break;
614         }
615     }
616
617     /**
618      * Method that initializes the titlebar of the activity.
619      */
620     private void initSearch() {
621         Serializable mimeTypeExtra = getIntent().getSerializableExtra(EXTRA_SEARCH_MIMETYPE);
622
623         if (mimeTypeExtra != null) {
624             MimeTypeCategory[] categories = (MimeTypeCategory[]) getIntent()
625                     .getSerializableExtra(EXTRA_SEARCH_MIMETYPE);
626             // setting load factor to 1 to avoid the backing map's resizing
627             mMimeTypeCategories = new HashSet<MimeTypeCategory>(categories.length, 1);
628             for (MimeTypeCategory category : categories) {
629                 mMimeTypeCategories.add(category);
630             }
631         }
632
633         //If data is not present, use root directory to do the search
634         this.mSearchDirectory = FileHelper.ROOT_DIRECTORY;
635         String searchDirectory = getIntent()
636                 .getStringExtra(SearchActivity.EXTRA_SEARCH_DIRECTORY);
637
638         if (!TextUtils.isEmpty(searchDirectory)) {
639             this.mSearchDirectory = searchDirectory;
640         }
641
642         //Retrieve the query Â¿from voice recognizer?
643         boolean voiceQuery = true;
644         List<String> userQueries =
645                 getIntent().getStringArrayListExtra(android.speech.RecognizerIntent.EXTRA_RESULTS);
646         if (userQueries == null || userQueries.size() == 0) {
647             //From input text
648             userQueries = new ArrayList<String>();
649             //Recovers and save the last term search in the memory
650             Preferences.setLastSearch(getIntent().getStringExtra(SearchManager.QUERY));
651             userQueries.add(Preferences.getLastSearch());
652             voiceQuery = false;
653         }
654
655         //Filter the queries? Needed if queries come from voice recognition
656         final List<String> filteredUserQueries =
657                 (voiceQuery) ? filterQuery(userQueries) : userQueries;
658
659         //Create the queries
660         this.mQuery = new Query().fillSlots(filteredUserQueries);
661         List<String> queries = this.mQuery.getQueries();
662
663         boolean ask = false;
664         // Mime type search uses '*' which needs to bypass
665         // length check
666         if (mMimeTypeCategories == null) {
667             //Check if some queries has lower than allowed, in this case
668             //request the user for stop the search
669             int cc = queries.size();
670             for (int i = 0; i < cc; i++) {
671                 if (queries.get(i).trim().length() < MIN_CHARS_SEARCH) {
672                     ask = true;
673                     break;
674                 }
675             }
676             mMimeTypeSpinner.setVisibility(View.VISIBLE);
677         } else {
678             mMimeTypeSpinner.setVisibility(View.INVISIBLE);
679         }
680         if (ask) {
681             askUserBeforeSearch(voiceQuery, this.mQuery, this.mSearchDirectory);
682         } else {
683             doSearch(voiceQuery, this.mQuery, this.mSearchDirectory);
684         }
685
686     }
687
688     /**
689      * Method that ask the user before do the search.
690      *
691      * @param voiceQuery Indicates if the query is from voice recognition
692      * @param query The terms of the search
693      * @param searchDirectory The directory of the search
694      */
695     private void askUserBeforeSearch(
696             final boolean voiceQuery, final Query query, final String searchDirectory) {
697         //Show a dialog asking the user
698         AlertDialog dialog =
699                 DialogHelper.createYesNoDialog(
700                         this,
701                         R.string.search_few_characters_title,
702                         R.string.search_few_characters_msg,
703                         new DialogInterface.OnClickListener() {
704                             @Override
705                             public void onClick(DialogInterface alertDialog, int which) {
706                                 if (which == DialogInterface.BUTTON_POSITIVE) {
707                                     doSearch(voiceQuery, query, searchDirectory);
708                                     return;
709                                 }
710
711                                 //Close search activity
712                                 back(true, null, false);
713                             }
714                        });
715         DialogHelper.delegateDialogShow(this, dialog);
716     }
717
718     /**
719      * Method that do the search.
720      *
721      * @param voiceQuery Indicates if the query is from voice recognition
722      * @param query The terms of the search
723      * @param searchDirectory The directory of the search
724      * @hide
725      */
726     void doSearch(
727             final boolean voiceQuery, final Query query, final String searchDirectory) {
728
729         // Recovers the user preferences about save suggestions
730         boolean saveSuggestions = Preferences.getSharedPreferences().getBoolean(
731                 FileManagerSettings.SETTINGS_SAVE_SEARCH_TERMS.getId(),
732                 ((Boolean) FileManagerSettings.SETTINGS_SAVE_SEARCH_TERMS.
733                         getDefaultValue()).booleanValue());
734         if (saveSuggestions) {
735             //Save every query for use as recent suggestions
736             SearchRecentSuggestions suggestions =
737                     new SearchRecentSuggestions(this,
738                             RecentSearchesContentProvider.AUTHORITY,
739                             RecentSearchesContentProvider.MODE);
740             if (!voiceQuery) {
741                 List<String> queries = query.getQueries();
742                 int cc = queries.size();
743                 for (int i = 0; i < cc; i++) {
744                     suggestions.saveRecentQuery(queries.get(i), null);
745                 }
746             }
747         }
748
749         //Set the listview
750         if (this.mSearchListView.getAdapter() != null) {
751             ((SearchResultAdapter)this.mSearchListView.getAdapter()).dispose();
752         }
753         this.mResultList = new ArrayList<FileSystemObject>();
754         mAdapter =
755                 new SearchResultAdapter(this,
756                         new ArrayList<SearchResult>(), R.layout.search_item, this.mQuery);
757         this.mSearchListView.setAdapter(mAdapter);
758
759         //Set terms
760         if (mMimeTypeCategories == null) {
761             this.mSearchTerms.setText(
762                     Html.fromHtml(getString(R.string.search_terms, query.getTerms())));
763         } else {
764             ArrayList<String> localizedNames = new ArrayList<String>(mMimeTypeCategories.size());
765             for (MimeTypeCategory category : mMimeTypeCategories) {
766                 localizedNames
767                         .add(NavigationActivity.MIME_TYPE_LOCALIZED_NAMES[category.ordinal()]);
768             }
769              this.mSearchTerms.setText(
770                      Html.fromHtml(getString(R.string.search_terms, localizedNames)));
771         }
772
773         //Now, do the search in background
774         this.mSearchListView.post(new Runnable() {
775             @Override
776             public void run() {
777                 try {
778                     // Execute the query (search in background)
779                     SearchActivity.this.mExecutable =
780                             CommandHelper.findFiles(
781                                     SearchActivity.this,
782                                     searchDirectory,
783                                     mQuery,
784                                     mAsyncListener,
785                                     null);
786
787                 } catch (Throwable ex) {
788                     //Remove all elements
789                     try {
790                         SearchActivity.this.removeAll();
791                     } catch (Throwable ex2) {
792                         /**NON BLOCK**/
793                     }
794
795                     //Capture the exception
796                     Log.e(TAG, "Search failed", ex); //$NON-NLS-1$
797                     DialogHelper.showToast(
798                             SearchActivity.this,
799                             R.string.search_error_msg, Toast.LENGTH_SHORT);
800                     SearchActivity.this.mSearchListView.setVisibility(View.GONE);
801                 }
802             }
803         });
804     }
805
806     /**
807      * Ensures the search result meets user preferences and passes it to the adapter for display
808      *
809      * @param result FileSystemObject that matches the search result criteria
810      */
811     private void showSearchResult(FileSystemObject result) {
812         // check against user's display preferences
813         if ( !FileHelper.compliesWithDisplayPreferences(result, null, mChRooted) ) {
814             return;
815         }
816
817         // resolve sym links
818         FileHelper.resolveSymlink(this, result);
819
820         // convert to search result
821         SearchResult searchResult = SearchHelper.convertToResult(result, mQuery);
822
823         // add to adapter
824         mAdapter.addNewItem(searchResult);
825     }
826
827     /**
828      * Method that restore the activity from the cached data.
829      */
830     private void loadFromCacheData() {
831         this.mSearchListView.post(new Runnable() {
832             @Override
833             public void run() {
834                 //Toggle results
835                 List<SearchResult> list = SearchActivity.this.mRestoreState.getSearchResultList();
836                 String directory = SearchActivity.this.mRestoreState.getSearchDirectory();
837                 SearchActivity.this.toggleResults(list.size() > 0, true);
838                 setFoundItems(list.size(), directory);
839
840                 //Set terms
841                 Query query = SearchActivity.this.mRestoreState.getSearchQuery();
842                 String terms =
843                         TextUtils.join(" | ",  //$NON-NLS-1$;
844                                 query.getQueries().toArray(new String[]{}));
845                 if (terms.endsWith(" | ")) { //$NON-NLS-1$;
846                     terms = ""; //$NON-NLS-1$;
847                 }
848                 SearchActivity.this.mSearchTerms.setText(
849                         Html.fromHtml(getString(R.string.search_terms, terms)));
850
851                 try {
852                     if (SearchActivity.this.mSearchWaiting != null) {
853                         SearchActivity.this.mSearchWaiting.setVisibility(View.VISIBLE);
854                     }
855
856                     //Add list to the listview
857                     if (SearchActivity.this.mSearchListView.getAdapter() != null) {
858                         ((SearchResultAdapter)SearchActivity.this.
859                                 mSearchListView.getAdapter()).clear();
860                     }
861                     SearchResultAdapter adapter =
862                             new SearchResultAdapter(
863                                                 SearchActivity.this.mSearchListView.getContext(),
864                                                 list,
865                                                 R.layout.search_item,
866                                                 query);
867                     SearchActivity.this.mSearchListView.setAdapter(adapter);
868                     SearchActivity.this.mSearchListView.setSelection(0);
869
870                     SearchActivity.this.mQuery = query;
871                     SearchActivity.this.mSearchDirectory = mRestoreState.getSearchDirectory();
872
873                 } catch (Throwable ex) {
874                     //Capture the exception
875                     ExceptionUtil.translateException(SearchActivity.this, ex);
876
877                 } finally {
878                     //Hide waiting
879                     if (SearchActivity.this.mSearchWaiting != null) {
880                         SearchActivity.this.mSearchWaiting.setVisibility(View.GONE);
881                     }
882                 }
883             }
884         });
885     }
886
887     /**
888      * Method that filter the user queries for valid queries only.<br/>
889      * <br/>
890      * Only allow query strings with more that 3 characters
891      *
892      * @param original The original user queries
893      * @return List<String> The list of queries filtered
894      */
895     @SuppressWarnings("static-method")
896     private List<String> filterQuery(List<String> original) {
897         List<String> dst = new ArrayList<String>(original);
898         int cc = dst.size();
899         for (int i = cc - 1; i >= 0; i--) {
900             String query = dst.get(i);
901             if (query == null || query.trim().length() < MIN_CHARS_SEARCH) {
902                 dst.remove(i);
903             }
904         }
905         return dst;
906     }
907
908     /**
909      * Method that removes all items and display a message.
910      * @hide
911      */
912     void removeAll() {
913         SearchResultAdapter adapter = (SearchResultAdapter)this.mSearchListView.getAdapter();
914         adapter.clear();
915         this.mSearchListView.setSelection(0);
916         toggleResults(false, true);
917     }
918
919     /**
920      * Method that toggle the views when there are results.
921      *
922      * @param hasResults Indicates if there are results
923      * @param showEmpty Show the empty list message
924      * @hide
925      */
926     void toggleResults(boolean hasResults, boolean showEmpty) {
927         this.mSearchListView.setVisibility(hasResults ? View.VISIBLE : View.INVISIBLE);
928         this.mEmptyListMsg.setVisibility(!hasResults && showEmpty ? View.VISIBLE : View.INVISIBLE);
929     }
930
931     /**
932      * Method that display the number of found items.
933      *
934      * @param items The number of items
935      * @param searchDirectory The search directory path
936      * @hide
937      */
938     void setFoundItems(final int items, final String searchDirectory) {
939         if (this.mSearchFoundItems != null) {
940             this.mSearchFoundItems.post(new Runnable() {
941                 @Override
942                 public void run() {
943                     String directory = searchDirectory;
944                     if (SearchActivity.this.mChRooted &&
945                             directory != null && directory.length() > 0) {
946                         directory = StorageHelper.getChrootedPath(directory);
947                     }
948
949                     String foundItems =
950                             getResources().
951                                 getQuantityString(
952                                     R.plurals.search_found_items, items, Integer.valueOf(items));
953                     SearchActivity.this.mSearchFoundItems.setText(
954                                             getString(
955                                                 R.string.search_found_items_in_directory,
956                                                 foundItems,
957                                                 directory));
958                 }
959             });
960         }
961     }
962
963     /**
964      * {@inheritDoc}
965      */
966     @Override
967     public boolean onKeyUp(int keyCode, KeyEvent event) {
968         switch (keyCode) {
969             case KeyEvent.KEYCODE_BACK:
970                 back(true, null, false);
971                 return true;
972             default:
973                 return super.onKeyUp(keyCode, event);
974         }
975     }
976
977     /**
978      * {@inheritDoc}
979      */
980     @Override
981     public boolean onOptionsItemSelected(MenuItem item) {
982        switch (item.getItemId()) {
983           case android.R.id.home:
984               back(true, null, false);
985               return true;
986           default:
987              return super.onOptionsItemSelected(item);
988        }
989     }
990
991     /**
992      * {@inheritDoc}
993      */
994     @Override
995     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
996         // cancel search query if in progress
997         // *need* to do this as the async query holds a lock on the Console and we need the Console
998         // to gather additional file info in order to process the click event
999         if (mSearchInProgress) mExecutable.cancel();
1000
1001         try {
1002             SearchResult result = ((SearchResultAdapter)parent.getAdapter()).getItem(position);
1003             FileSystemObject fso = result.getFso();
1004             if (fso instanceof Directory) {
1005                 back(false, fso, false);
1006                 return;
1007             } else if (fso instanceof Symlink) {
1008                 Symlink symlink = (Symlink)fso;
1009                 if (symlink.getLinkRef() != null && symlink.getLinkRef() instanceof Directory) {
1010                     back(false, symlink.getLinkRef(), false);
1011                     return;
1012                 }
1013                 fso = symlink.getLinkRef();
1014             }
1015
1016             // Open the file with the preferred registered app
1017             back(false, fso, false);
1018
1019         } catch (Throwable ex) {
1020             ExceptionUtil.translateException(this.mSearchListView.getContext(), ex);
1021         }
1022     }
1023
1024     /**
1025      * {@inheritDoc}
1026      */
1027     @Override
1028     public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
1029         // Different actions depending on user preference
1030
1031         // Get the adapter, the search result and the fso
1032         SearchResultAdapter adapter = ((SearchResultAdapter)parent.getAdapter());
1033         SearchResult searchResult = adapter.getItem(position);
1034         FileSystemObject fso = searchResult.getFso();
1035
1036         // Open the actions menu
1037         onRequestMenu(fso);
1038         return true; //Always consume the event
1039     }
1040
1041     /**
1042      * Method invoked when a request to show the menu associated
1043      * with an item is started.
1044      *
1045      * @param item The item for which the request was started
1046      */
1047     public void onRequestMenu(FileSystemObject item) {
1048         // Prior to show the dialog, refresh the item reference
1049         FileSystemObject fso = null;
1050         try {
1051             fso = CommandHelper.getFileInfo(this, item.getFullPath(), false, null);
1052             if (fso == null) {
1053                 throw new NoSuchFileOrDirectory(item.getFullPath());
1054             }
1055
1056         } catch (Exception e) {
1057             // Notify the user
1058             ExceptionUtil.translateException(this, e);
1059
1060             // Remove the object
1061             if (e instanceof FileNotFoundException || e instanceof NoSuchFileOrDirectory) {
1062                 removeItem(item);
1063             }
1064             return;
1065         }
1066
1067         ActionsDialog dialog = new ActionsDialog(this, null, fso, false, true);
1068         dialog.setOnRequestRefreshListener(this);
1069         dialog.show();
1070     }
1071
1072     /**
1073      * Method that removes the {@link FileSystemObject} reference
1074      *
1075      * @param fso The file system object
1076      */
1077     private void removeItem(FileSystemObject fso) {
1078         SearchResultAdapter adapter =
1079                 (SearchResultAdapter)this.mSearchListView.getAdapter();
1080         if (adapter != null) {
1081             int pos = adapter.getPosition(fso);
1082             if (pos != -1) {
1083                 SearchResult sr = adapter.getItem(pos);
1084                 adapter.remove(sr);
1085             }
1086
1087             // Toggle resultset?
1088             toggleResults(adapter.getCount() > 0, true);
1089             setFoundItems(adapter.getCount(), this.mSearchDirectory);
1090         }
1091     }
1092
1093     /**
1094      * {@inheritDoc}
1095      */
1096     @Override
1097     public void onRequestRefresh(Object o, boolean clearSelection) {
1098         // Refresh only the item
1099         SearchResultAdapter adapter =
1100                 (SearchResultAdapter)this.mSearchListView.getAdapter();
1101         if (adapter != null) {
1102             if (o instanceof FileSystemObject) {
1103
1104                 FileSystemObject fso = (FileSystemObject)o;
1105                 int pos = adapter.getPosition(fso);
1106                 if (pos >= 0) {
1107                     SearchResult sr = adapter.getItem(pos);
1108                     sr.setFso(fso);
1109                 }
1110             } else if (o == null) {
1111                 // Refresh all
1112                 List<SearchResult> results = adapter.getData();
1113                 this.mResultList = new ArrayList<FileSystemObject>(results.size());
1114                 int cc = results.size();
1115                 for (int i = 0; i < cc; i++) {
1116                     this.mResultList.add(results.get(i).getFso());
1117                 }
1118             }
1119         }
1120     }
1121
1122     /**
1123      * {@inheritDoc}
1124      */
1125     @Override
1126     public void onRequestBookmarksRefresh() {
1127         // Ignore
1128     }
1129
1130     /**
1131      * {@inheritDoc}
1132      */
1133     @Override
1134     public void onRequestRemove(Object o, boolean clearSelection) {
1135         if (o instanceof FileSystemObject) {
1136             removeItem((FileSystemObject) o);
1137         }
1138     }
1139
1140     /**
1141      * {@inheritDoc}
1142      */
1143     @Override
1144     public void onNavigateTo(Object o) {
1145         if (o instanceof FileSystemObject) {
1146             back(false, (FileSystemObject) o, true);
1147         }
1148     }
1149
1150     /**
1151      * Method that returns to previous activity.
1152      *
1153      * @param cancelled Indicates if the activity was cancelled
1154      * @param item The fso
1155      * @param isChecked If the fso was fully retrieve previously to this call. Otherwise, a
1156      * getFileInfo call is done to complete the fso information
1157      * @hide
1158      */
1159     void back(final boolean cancelled, FileSystemObject item, boolean isChecked) {
1160         final Context ctx = SearchActivity.this;
1161         boolean finish = true;
1162         if (cancelled) {
1163             final Intent intent =  new Intent();
1164             if (this.mRestoreState != null) {
1165                 Bundle bundle = new Bundle();
1166                 bundle.putParcelable(NavigationActivity.EXTRA_SEARCH_LAST_SEARCH_DATA,
1167                         (Parcelable)this.mRestoreState);
1168                 intent.putExtras(bundle);
1169             }
1170             setResult(RESULT_CANCELED, intent);
1171         } else {
1172             // Check that the bookmark exists
1173             FileSystemObject fso = item;
1174             try {
1175                 if (!isChecked) {
1176                     fso = CommandHelper.getFileInfo(ctx, item.getFullPath(), null);
1177                 }
1178                 finish = navigateTo(fso);
1179
1180             } catch (Exception e) {
1181                 // Capture the exception
1182                 final FileSystemObject fFso = fso;
1183                 final OnRelaunchCommandResult relaunchListener = new OnRelaunchCommandResult() {
1184                     @Override
1185                     public void onSuccess() {
1186                         if (navigateTo(fFso)) {
1187                             exit();
1188                         }
1189                     }
1190                     @Override
1191                     public void onFailed(Throwable cause) {
1192                         ExceptionUtil.translateException(ctx, cause, false, false);
1193                     }
1194                     @Override
1195                     public void onCancelled() { /** NON BLOCK**/}
1196                 };
1197                 ExceptionUtil.translateException(ctx, e, false, true, relaunchListener);
1198                 if (!(e instanceof RelaunchableException)) {
1199                     if (e instanceof NoSuchFileOrDirectory || e instanceof FileNotFoundException) {
1200                         // The fso not exists, delete the fso from the search
1201                         try {
1202                             removeItem(fso);
1203                         } catch (Exception ex) {/**NON BLOCK**/}
1204                     }
1205                 }
1206                 return;
1207             }
1208         }
1209
1210         // End this activity
1211         if (finish) {
1212             exit();
1213         }
1214     }
1215
1216     /**
1217      * Method invoked when the activity needs to exit
1218      */
1219     private void exit() {
1220         if (this.mSearchListView.getAdapter() != null) {
1221             ((SearchResultAdapter)this.mSearchListView.getAdapter()).dispose();
1222         }
1223         finish();
1224     }
1225
1226     /**
1227      * Method that navigate to the file system used the intent (NavigationActivity)
1228      *
1229      * @param fso The file system object to navigate to
1230      * @param intent The intent used to navigate to
1231      * @return boolean If the action implies finish this activity
1232      */
1233     boolean navigateTo(FileSystemObject fso) {
1234         if (fso != null) {
1235             if (FileHelper.isDirectory(fso)) {
1236                 final Intent intent = new Intent();
1237                 Bundle bundle = new Bundle();
1238                 bundle.putSerializable(NavigationActivity.EXTRA_SEARCH_ENTRY_SELECTION, fso);
1239                 bundle.putParcelable(NavigationActivity.EXTRA_SEARCH_LAST_SEARCH_DATA,
1240                         (Parcelable)createSearchInfo());
1241                 intent.putExtras(bundle);
1242                 setResult(RESULT_OK, intent);
1243                 return true;
1244             }
1245
1246             // Open the file here, so when focus back to the app, the search activity
1247             // its in top of the stack
1248             IntentsActionPolicy.openFileSystemObject(this, fso, false, null, null);
1249         } else {
1250             // The fso not exists, delete the fso from the search
1251             try {
1252                 removeItem(fso);
1253             } catch (Exception ex) {/**NON BLOCK**/}
1254         }
1255         return false;
1256     }
1257
1258     /**
1259      * Method that creates a {@link SearchInfoParcelable} reference from
1260      * the current data.
1261      *
1262      * @return SearchInfoParcelable The search info reference
1263      */
1264     private SearchInfoParcelable createSearchInfo() {
1265         SearchInfoParcelable parcel = new SearchInfoParcelable(
1266                 mSearchDirectory,
1267                 ((SearchResultAdapter)this.mSearchListView.getAdapter()).getData(),
1268                 mQuery);
1269         return parcel;
1270     }
1271
1272     /**
1273      * Method that applies the current theme to the activity
1274      * @hide
1275      */
1276     void applyTheme() {
1277         Theme theme = ThemeManager.getCurrentTheme(this);
1278         theme.setBaseTheme(this, false);
1279
1280         //- ActionBar
1281         View v = getActionBar().getCustomView().findViewById(R.id.customtitle_title);
1282         theme.setTextColor(this, (TextView)v, "action_bar_text_color"); //$NON-NLS-1$
1283         v = findViewById(R.id.ab_button1);
1284         theme.setImageDrawable(this, (ImageView)v, "ic_config_drawable"); //$NON-NLS-1$
1285         // ContentView
1286         theme.setBackgroundDrawable(
1287                 this, getWindow().getDecorView(), "background_drawable"); //$NON-NLS-1$
1288         //- StatusBar
1289         v = findViewById(R.id.search_status);
1290         theme.setBackgroundDrawable(this, v, "statusbar_drawable"); //$NON-NLS-1$
1291         v = findViewById(R.id.search_status_found_items);
1292         theme.setTextColor(this, (TextView)v, "action_bar_text_color"); //$NON-NLS-1$
1293         v = findViewById(R.id.search_status_query_terms);
1294         theme.setTextColor(this, (TextView)v, "action_bar_text_color"); //$NON-NLS-1$
1295
1296         //ListView
1297         if (this.mSearchListView.getAdapter() != null) {
1298             ((SearchResultAdapter)this.mSearchListView.getAdapter()).notifyDataSetChanged();
1299         }
1300         this.mSearchListView.setDivider(
1301                 theme.getDrawable(this, "horizontal_divider_drawable")); //$NON-NLS-1$
1302         this.mSearchListView.invalidate();
1303     }
1304
1305     @Override
1306     public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
1307         String category = MimeTypeHelper.MimeTypeCategory.names()[i];
1308         SearchResultAdapter adapter = ((SearchResultAdapter) this.mSearchListView.getAdapter());
1309         if (adapter != null) {
1310             adapter.setMimeFilter(category);
1311         }
1312     }
1313
1314     @Override
1315     public void onNothingSelected(AdapterView<?> adapterView) {
1316         //ignore
1317     }
1318 }
1319