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.server.search;
18 
19 import android.app.SearchManager;
20 import android.app.SearchableInfo;
21 import android.app.SearchableInfo.ActionKeyInfo;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ActivityInfo;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ProviderInfo;
29 import android.content.pm.ResolveInfo;
30 import android.content.res.Resources;
31 import android.content.res.XmlResourceParser;
32 import android.os.RemoteException;
33 import com.android.server.search.Searchables;
34 import android.test.AndroidTestCase;
35 import android.test.MoreAsserts;
36 import android.test.mock.MockContext;
37 import android.test.mock.MockPackageManager;
38 import android.test.suitebuilder.annotation.SmallTest;
39 import android.view.KeyEvent;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 /**
45  * To launch this test from the command line:
46  *
47  * adb shell am instrument -w \
48  *   -e class com.android.unit_tests.SearchablesTest \
49  *   com.android.unit_tests/android.test.InstrumentationTestRunner
50  */
51 @SmallTest
52 public class SearchablesTest extends AndroidTestCase {
53 
54     /*
55      * SearchableInfo tests
56      *  Mock the context so I can provide very specific input data
57      *  Confirm OK with "zero" searchables
58      *  Confirm "good" metadata read properly
59      *  Confirm "bad" metadata skipped properly
60      *  Confirm ordering of searchables
61      *  Confirm "good" actionkeys
62      *  confirm "bad" actionkeys are rejected
63      *  confirm XML ordering enforced (will fail today - bug in SearchableInfo)
64      *  findActionKey works
65      *  getIcon works
66      */
67 
68     /**
69      * Test that non-searchable activities return no searchable info (this would typically
70      * trigger the use of the default searchable e.g. contacts)
71      */
testNonSearchable()72     public void testNonSearchable() {
73         // test basic array & hashmap
74         Searchables searchables = new Searchables(mContext, 0);
75         searchables.updateSearchableList();
76 
77         // confirm that we return null for non-searchy activities
78         ComponentName nonActivity = new ComponentName(
79                             "com.android.frameworks.coretests",
80                             "com.android.frameworks.coretests.activity.NO_SEARCH_ACTIVITY");
81         SearchableInfo si = searchables.getSearchableInfo(nonActivity);
82         assertNull(si);
83     }
84 
85     /**
86      * This is an attempt to run the searchable info list with a mocked context.  Here are some
87      * things I'd like to test.
88      *
89      *  Confirm OK with "zero" searchables
90      *  Confirm "good" metadata read properly
91      *  Confirm "bad" metadata skipped properly
92      *  Confirm ordering of searchables
93      *  Confirm "good" actionkeys
94      *  confirm "bad" actionkeys are rejected
95      *  confirm XML ordering enforced (will fail today - bug in SearchableInfo)
96      *  findActionKey works
97      *  getIcon works
98 
99      */
testSearchablesListReal()100     public void testSearchablesListReal() {
101         MyMockPackageManager mockPM = new MyMockPackageManager(mContext.getPackageManager());
102         MyMockContext mockContext = new MyMockContext(mContext, mockPM);
103 
104         // build item list with real-world source data
105         mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_PASSTHROUGH);
106         Searchables searchables = new Searchables(mockContext, 0);
107         searchables.updateSearchableList();
108         // tests with "real" searchables (deprecate, this should be a unit test)
109         ArrayList<SearchableInfo> searchablesList = searchables.getSearchablesList();
110         int count = searchablesList.size();
111         assertTrue(count >= 1);         // this isn't really a unit test
112         checkSearchables(searchablesList);
113         ArrayList<SearchableInfo> global = searchables.getSearchablesInGlobalSearchList();
114         checkSearchables(global);
115     }
116 
117     /**
118      * This round of tests confirms good operations with "zero" searchables found
119      */
testSearchablesListEmpty()120     public void testSearchablesListEmpty() {
121         MyMockPackageManager mockPM = new MyMockPackageManager(mContext.getPackageManager());
122         MyMockContext mockContext = new MyMockContext(mContext, mockPM);
123 
124         mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_MOCK_ZERO);
125         Searchables searchables = new Searchables(mockContext, 0);
126         searchables.updateSearchableList();
127         ArrayList<SearchableInfo> searchablesList = searchables.getSearchablesList();
128         assertNotNull(searchablesList);
129         MoreAsserts.assertEmpty(searchablesList);
130         ArrayList<SearchableInfo> global = searchables.getSearchablesInGlobalSearchList();
131         MoreAsserts.assertEmpty(global);
132     }
133 
134     /**
135      * Generic health checker for an array of searchables.
136      *
137      * This is designed to pass for any semi-legal searchable, without knowing much about
138      * the format of the underlying data.  It's fairly easy for a non-compliant application
139      * to provide meta-data that will pass here (e.g. a non-existent suggestions authority).
140      *
141      * @param searchables The list of searchables to examine.
142      */
checkSearchables(ArrayList<SearchableInfo> searchablesList)143     private void checkSearchables(ArrayList<SearchableInfo> searchablesList) {
144         assertNotNull(searchablesList);
145         int count = searchablesList.size();
146         for (int ii = 0; ii < count; ii++) {
147             SearchableInfo si = searchablesList.get(ii);
148             checkSearchable(si);
149         }
150     }
151 
checkSearchable(SearchableInfo si)152     private void checkSearchable(SearchableInfo si) {
153         assertNotNull(si);
154         assertTrue(si.getLabelId() != 0);        // This must be a useable string
155         assertNotEmpty(si.getSearchActivity().getClassName());
156         assertNotEmpty(si.getSearchActivity().getPackageName());
157         if (si.getSuggestAuthority() != null) {
158             // The suggestion fields are largely optional, so we'll just confirm basic health
159             assertNotEmpty(si.getSuggestAuthority());
160             assertNullOrNotEmpty(si.getSuggestPath());
161             assertNullOrNotEmpty(si.getSuggestSelection());
162             assertNullOrNotEmpty(si.getSuggestIntentAction());
163             assertNullOrNotEmpty(si.getSuggestIntentData());
164         }
165         /* Add a way to get the entire action key list, then explicitly test its elements */
166         /* For now, test the most common action key (CALL) */
167         ActionKeyInfo ai = si.findActionKey(KeyEvent.KEYCODE_CALL);
168         if (ai != null) {
169             assertEquals(ai.getKeyCode(), KeyEvent.KEYCODE_CALL);
170             // one of these three fields must be non-null & non-empty
171             boolean m1 = (ai.getQueryActionMsg() != null) && (ai.getQueryActionMsg().length() > 0);
172             boolean m2 = (ai.getSuggestActionMsg() != null) && (ai.getSuggestActionMsg().length() > 0);
173             boolean m3 = (ai.getSuggestActionMsgColumn() != null) &&
174                             (ai.getSuggestActionMsgColumn().length() > 0);
175             assertTrue(m1 || m2 || m3);
176         }
177 
178         /*
179          * Find ways to test these:
180          *
181          * private int mSearchMode
182          * private Drawable mIcon
183          */
184 
185         /*
186          * Explicitly not tested here:
187          *
188          * Can be null, so not much to see:
189          * public String mSearchHint
190          * private String mZeroQueryBanner
191          *
192          * To be deprecated/removed, so don't bother:
193          * public boolean mFilterMode
194          * public boolean mQuickStart
195          * private boolean mIconResized
196          * private int mIconResizeWidth
197          * private int mIconResizeHeight
198          *
199          * All of these are "internal" working variables, not part of any contract
200          * private ActivityInfo mActivityInfo
201          * private Rect mTempRect
202          * private String mSuggestProviderPackage
203          * private String mCacheActivityContext
204          */
205     }
206 
207     /**
208      * Combo assert for "string not null and not empty"
209      */
assertNotEmpty(final String s)210     private void assertNotEmpty(final String s) {
211         assertNotNull(s);
212         MoreAsserts.assertNotEqual(s, "");
213     }
214 
215     /**
216      * Combo assert for "string null or (not null and not empty)"
217      */
assertNullOrNotEmpty(final String s)218     private void assertNullOrNotEmpty(final String s) {
219         if (s != null) {
220             MoreAsserts.assertNotEqual(s, "");
221         }
222     }
223 
224     /**
225      * This is a mock for context.  Used to perform a true unit test on SearchableInfo.
226      *
227      */
228     private class MyMockContext extends MockContext {
229 
230         protected Context mRealContext;
231         protected PackageManager mPackageManager;
232 
233         /**
234          * Constructor.
235          *
236          * @param realContext Please pass in a real context for some pass-throughs to function.
237          */
MyMockContext(Context realContext, PackageManager packageManager)238         MyMockContext(Context realContext, PackageManager packageManager) {
239             mRealContext = realContext;
240             mPackageManager = packageManager;
241         }
242 
243         /**
244          * Resources.  Pass through for now.
245          */
246         @Override
getResources()247         public Resources getResources() {
248             return mRealContext.getResources();
249         }
250 
251         /**
252          * Package manager.  Pass through for now.
253          */
254         @Override
getPackageManager()255         public PackageManager getPackageManager() {
256             return mPackageManager;
257         }
258 
259         /**
260          * Package manager.  Pass through for now.
261          */
262         @Override
createPackageContext(String packageName, int flags)263         public Context createPackageContext(String packageName, int flags)
264                 throws PackageManager.NameNotFoundException {
265             return mRealContext.createPackageContext(packageName, flags);
266         }
267 
268         /**
269          * Message broadcast.  Pass through for now.
270          */
271         @Override
sendBroadcast(Intent intent)272         public void sendBroadcast(Intent intent) {
273             mRealContext.sendBroadcast(intent);
274         }
275     }
276 
277 /**
278  * This is a mock for package manager.  Used to perform a true unit test on SearchableInfo.
279  *
280  */
281     private class MyMockPackageManager extends MockPackageManager {
282 
283         public final static int SEARCHABLES_PASSTHROUGH = 0;
284         public final static int SEARCHABLES_MOCK_ZERO = 1;
285         public final static int SEARCHABLES_MOCK_ONEGOOD = 2;
286         public final static int SEARCHABLES_MOCK_ONEGOOD_ONEBAD = 3;
287 
288         protected PackageManager mRealPackageManager;
289         protected int mSearchablesMode;
290 
MyMockPackageManager(PackageManager realPM)291         public MyMockPackageManager(PackageManager realPM) {
292             mRealPackageManager = realPM;
293             mSearchablesMode = SEARCHABLES_PASSTHROUGH;
294         }
295 
296         /**
297          * Set the mode for various tests.
298          */
setSearchablesMode(int newMode)299         public void setSearchablesMode(int newMode) {
300             switch (newMode) {
301             case SEARCHABLES_PASSTHROUGH:
302             case SEARCHABLES_MOCK_ZERO:
303                 mSearchablesMode = newMode;
304                 break;
305 
306             default:
307                 throw new UnsupportedOperationException();
308             }
309         }
310 
311         /**
312          * Find activities that support a given intent.
313          *
314          * Retrieve all activities that can be performed for the given intent.
315          *
316          * @param intent The desired intent as per resolveActivity().
317          * @param flags Additional option flags.  The most important is
318          *                    MATCH_DEFAULT_ONLY, to limit the resolution to only
319          *                    those activities that support the CATEGORY_DEFAULT.
320          *
321          * @return A List<ResolveInfo> containing one entry for each matching
322          *         Activity. These are ordered from best to worst match -- that
323          *         is, the first item in the list is what is returned by
324          *         resolveActivity().  If there are no matching activities, an empty
325          *         list is returned.
326          */
327         @Override
queryIntentActivities(Intent intent, int flags)328         public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
329             assertNotNull(intent);
330             assertTrue(intent.getAction().equals(Intent.ACTION_SEARCH)
331                     || intent.getAction().equals(Intent.ACTION_WEB_SEARCH)
332                     || intent.getAction().equals(SearchManager.INTENT_ACTION_GLOBAL_SEARCH));
333             switch (mSearchablesMode) {
334             case SEARCHABLES_PASSTHROUGH:
335                 return mRealPackageManager.queryIntentActivities(intent, flags);
336             case SEARCHABLES_MOCK_ZERO:
337                 return null;
338             default:
339                 throw new UnsupportedOperationException();
340             }
341         }
342 
343         @Override
resolveActivity(Intent intent, int flags)344         public ResolveInfo resolveActivity(Intent intent, int flags) {
345             assertNotNull(intent);
346             assertTrue(intent.getAction().equals(Intent.ACTION_WEB_SEARCH)
347                     || intent.getAction().equals(SearchManager.INTENT_ACTION_GLOBAL_SEARCH));
348             switch (mSearchablesMode) {
349             case SEARCHABLES_PASSTHROUGH:
350                 return mRealPackageManager.resolveActivity(intent, flags);
351             case SEARCHABLES_MOCK_ZERO:
352                 return null;
353             default:
354                 throw new UnsupportedOperationException();
355             }
356         }
357 
358         /**
359          * Retrieve an XML file from a package.  This is a low-level API used to
360          * retrieve XML meta data.
361          *
362          * @param packageName The name of the package that this xml is coming from.
363          * Can not be null.
364          * @param resid The resource identifier of the desired xml.  Can not be 0.
365          * @param appInfo Overall information about <var>packageName</var>.  This
366          * may be null, in which case the application information will be retrieved
367          * for you if needed; if you already have this information around, it can
368          * be much more efficient to supply it here.
369          *
370          * @return Returns an TypedXmlPullParser allowing you to parse out the XML
371          * data.  Returns null if the xml resource could not be found for any
372          * reason.
373          */
374         @Override
getXml(String packageName, int resid, ApplicationInfo appInfo)375         public XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) {
376             assertNotNull(packageName);
377             MoreAsserts.assertNotEqual(packageName, "");
378             MoreAsserts.assertNotEqual(resid, 0);
379             switch (mSearchablesMode) {
380             case SEARCHABLES_PASSTHROUGH:
381                 return mRealPackageManager.getXml(packageName, resid, appInfo);
382             case SEARCHABLES_MOCK_ZERO:
383             default:
384                 throw new UnsupportedOperationException();
385             }
386         }
387 
388         /**
389          * Find a single content provider by its base path name.
390          *
391          * @param name The name of the provider to find.
392          * @param flags Additional option flags.  Currently should always be 0.
393          *
394          * @return ContentProviderInfo Information about the provider, if found,
395          *         else null.
396          */
397         @Override
resolveContentProvider(String name, int flags)398         public ProviderInfo resolveContentProvider(String name, int flags) {
399             assertNotNull(name);
400             MoreAsserts.assertNotEqual(name, "");
401             assertEquals(flags, 0);
402             switch (mSearchablesMode) {
403             case SEARCHABLES_PASSTHROUGH:
404                 return mRealPackageManager.resolveContentProvider(name, flags);
405             case SEARCHABLES_MOCK_ZERO:
406             default:
407                 throw new UnsupportedOperationException();
408             }
409         }
410 
411         /**
412          * Get the activity information for a particular activity.
413          *
414          * @param name The name of the activity to find.
415          * @param flags Additional option flags.
416          *
417          * @return ActivityInfo Information about the activity, if found, else null.
418          */
419         @Override
getActivityInfo(ComponentName name, int flags)420         public ActivityInfo getActivityInfo(ComponentName name, int flags)
421                 throws NameNotFoundException {
422             assertNotNull(name);
423             MoreAsserts.assertNotEqual(name, "");
424             switch (mSearchablesMode) {
425             case SEARCHABLES_PASSTHROUGH:
426                 return mRealPackageManager.getActivityInfo(name, flags);
427             case SEARCHABLES_MOCK_ZERO:
428                 throw new NameNotFoundException();
429             default:
430                 throw new UnsupportedOperationException();
431             }
432         }
433 
434         @Override
checkPermission(String permName, String pkgName)435         public int checkPermission(String permName, String pkgName) {
436             assertNotNull(permName);
437             assertNotNull(pkgName);
438             switch (mSearchablesMode) {
439                 case SEARCHABLES_PASSTHROUGH:
440                     return mRealPackageManager.checkPermission(permName, pkgName);
441                 case SEARCHABLES_MOCK_ZERO:
442                     return PackageManager.PERMISSION_DENIED;
443                 default:
444                     throw new UnsupportedOperationException();
445                 }
446         }
447     }
448 }
449 
450