After searching a lot on the web, including stackoverflow, I still cannot resolve my problem : an activity cannot start because it does not find the required information for the intent.
Context
While searching for songs information in an xml file loaded in a list of Song objects, I am using the google suggestions engine and thus a ContentProvider class to deal with the logic.
Questions
1) Do I need to add mimeType information in the manifest and the ContentProvider ? When I try, the suggestion fail. 2) What do I need to put exactly in the URI variable given I do no use a database but a MatrixCursor instead ?
Code
1) Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="songs4heaven_p2.com.hfad.songs4heaven_p2">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainSongs4Heaven_p2"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Points to searchable activity so the whole app can invoke search. -->
<meta-data
android:name="android.app.default_searchable"
android:value=".SearchAndroidActivity" />
</activity>
<!-- Ancien système de recherche : Le Main appelle SearchActivity via l icone de recherche
puis SearcActivity appelle SearchListActivity au clic dans SearchActivity -->
<!-- Nouveau système de recherche par le mécanisme natif Android
L'icône de recherche est dans la barre d icone en haut à droite
et appelle une searchable activity décrite dans le AndroidManifest.xml -->
<activity android:name=".SearchAndroidActivity"
android:launchMode="singleTop" >
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.SEARCH" />
<action android:name="android.intent.action.VIEW" />
<!--<data android:mimeType="vnd.android.cursor.item/vnd.songs4heaven.search.songs" />-->
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<!-- Points to searchable activity so the whole app can invoke search. -->
<meta-data
android:name="android.app.defaul_searchable"
android:value="songs4heaven_p2.com.hfad.songs4heaven_p2.SearchAndroidActivity" />
<provider
android:name="songs4heaven_p2.com.hfad.songs4heaven_p2.SearchContentProvider"
android:authorities="songs4heaven_p2.com.hfad.songs4heaven_p2.SearchContentProvider"
android:multiprocess="true">
</provider>
</application>
</manifest>
2) ContenProvider
package songs4heaven_p2.com.hfad.songs4heaven_p2;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.Log;
import java.util.List;
public class SearchContentProvider extends ContentProvider{
public List<Song> songs = null;
public static final String BASE_DATA_NAME = "song";
private static final String AUTHORITY = "songs4heaven_p2.com.hfad.songs4heaven_p2.SearchContentProvider";
// Dans le manifeste :
// data android:mimeType="vnd.android.cursor.item/vnd.songs4heaven.search.songs"
//public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.songs4heaven.search." + BASE_DATA_NAME;
//public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.songs4heaven.search." + BASE_DATA_NAME;
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_DATA_NAME);
public static final Uri SEARCH_SUGGEST_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_DATA_NAME + "/" + SearchManager.SUGGEST_URI_PATH_QUERY);
private static final int TYPE_ALL_SUGGESTIONS = 1;
private static final int TYPE_SINGLE_SUGGESTION = 2;
private UriMatcher mUriMatcher;
public List<Song> mSongs = null;
private static String[] matrixCursorColumns = {"_ID",
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_INTENT_DATA };
@Override
public boolean onCreate() {
Log.i("DEBUG", "Entree SearchContentProvider / onCreate ");
//Log.i("DEBUG", "onCreate / CONTENT_ITEM_TYPE:"+CONTENT_ITEM_TYPE);
//Log.i("DEBUG", "onCreate / CONTENT_TYPE:"+CONTENT_TYPE);
Log.i("DEBUG", "onCreate / CONTENT_ITEM_BASE_TYPE:"+ContentResolver.CURSOR_ITEM_BASE_TYPE);
Log.i("DEBUG", "onCreate / CONTENT_DIR_BASE_TYPE:"+ContentResolver.CURSOR_DIR_BASE_TYPE);
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//mUriMatcher.addURI(AUTHORITY, "/#", TYPE_SINGLE_SUGGESTION);
mUriMatcher.addURI(AUTHORITY, "search_suggest_query/*", TYPE_ALL_SUGGESTIONS);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
//https://developer.android.com/guide/topics/providers/content-provider-creating#ContentProvider
//For example, the MatrixCursor class implements a cursor in which each row is an array of Object. With this class, use addRow() to add a new row.
Log.i("DEBUG", "Entree Cursor / uri:"+uri);
String queryType = "";
switch(mUriMatcher.match(uri)){
case 1 :
Log.i("DEBUG", "Cursor / case : 1");
String query = uri.getLastPathSegment().toLowerCase();
return getSearchResultsCursor(query);
default:
Log.i("DEBUG", "Cursor / case : null");
return null;
}
}
private MatrixCursor getSearchResultsCursor(String searchString){
MatrixCursor searchResults = new MatrixCursor(matrixCursorColumns);
Object[] mRow = new Object[3];
int counterId = 0;
if(searchString != null){
searchString = searchString.toLowerCase();
//String v_titre = "";
//for (int i = 0; i < songs.size(); i++) {
//v_titre = songs.get( i )._title;
//}
mSongs = SingleLoadXml.getSongs( getContext());
for (int i = 0; i < mSongs.size(); i++) {
int id = mSongs.get( i )._ID;
String title = mSongs.get( i )._title;
String words = mSongs.get( i )._words;
Log.i("DEBUG", "Boucle MatrixCursor avec title:"+title);
if (title.toLowerCase().contains( searchString )) {
mRow[0] = "" + counterId++;
mRow[1] = title;
mRow[2] = "" + counterId++;
Log.i("DEBUG", "Boucle MatrixCursor contains avec mRow:"+mRow+":title:"+title);
searchResults.addRow( mRow );
}
}
}
Log.i("DEBUG", "Boucle MatrixCursor avant return: mRow:"+searchResults);
return searchResults; }
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public String getType(Uri uri) {
switch (mUriMatcher.match( uri )) {
case TYPE_ALL_SUGGESTIONS:
return SearchManager.SUGGEST_MIME_TYPE;
default:
throw new IllegalArgumentException( "Unknown URL " + uri );
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException("Not yet implemented");
}
}
3) searchable.xml
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name"
android:searchSuggestAuthority="songs4heaven_p2.com.hfad.songs4heaven_p2.SearchContentProvider"
android:searchSuggestIntentAction="android.intent.action.VIEW"
android:searchSuggestIntentData="content://songs4heaven_p2.com.hfad.songs4heaven_p2.SearchContentProvider/songs"
android:searchSuggestThreshold="2"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer">
</searchable>
4) SearchAndroidActivity for the Intent managing
package songs4heaven_p2.com.hfad.songs4heaven_p2;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class SearchAndroidActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
private static final String TAG = SearchAndroidActivity.class.getSimpleName();
private ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.activity_searchable);
// 20180802 : cast obligatoire sinon il voit du View alors que c'est bien du ListView ...
listView = (ListView) findViewById( R.id.search_listview );
listView.setOnItemClickListener(this);
Log.i(TAG, "DEBUG: onCreate avant handleIntent");
Log.i(TAG, "DEBUG: getAction:" + getIntent().getAction());
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent newIntent) {
// update the activity launch intent
setIntent(newIntent);
// handle it
Log.i(TAG, "DEBUG: onNewIntent");
handleIntent(newIntent);
}
private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
// The user has initiated a search.
Log.i(TAG, "handleIntent: Intent.ACTION_SEARCH");
String query = intent.getStringExtra(SearchManager.QUERY);
Log.i(TAG, "DEBUG: handleIntent: query:"+query+":");
doSearch(query);
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
// The user has selected a suggestion.
// Handles clicking on the search results drop down...
Log.i(TAG, "DEBUG : handleIntent: Intent.ACTION_VIEW");
Uri details = intent.getData();
String details2 = intent.getDataString();
ComponentName details3 = intent.getComponent();
String id = intent.getStringExtra("id");
String name = intent.getStringExtra("name");
ComponentName resolve = intent.resolveActivity( getPackageManager() );
Log.i(TAG, "DEBUG : URI:details:"+details.getLastPathSegment());
Log.i(TAG, "DEBUG : URI:details2:"+details2);
Log.i(TAG, "DEBUG : URI:details3:"+details3);
Log.i(TAG, "DEBUG : URI:details full:"+details);
Log.i(TAG, "DEBUG : URI:id:"+id);
Log.i(TAG, "DEBUG : URI:name:"+name);
Log.i(TAG, "DEBUG : resolve:"+resolve);
Intent detailsIntent = new Intent(Intent.ACTION_VIEW, details);
//Intent detailsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse( details2 ) );
try {
startActivity(detailsIntent);
} catch ( ActivityNotFoundException e) {
e.printStackTrace();
Log.e(TAG,"DEBUG : Can't find activity to handle intent; ignoring",e);
}
//startActivity(Intent.createChooser(detailsIntent, "dialogTitle"));
//finish();
}
}
private void doSearch(String query) {
Log.i(TAG, "DEBUG : Entree - doSearch");
new SearchTask().execute(query);
}
private class SearchTask extends AsyncTask<String, Void, Cursor> {
@Override
protected Cursor doInBackground(String... params) {
Log.i(TAG, "DEBUG : Entree - doInBackground");
String wildcardQuery = "%" + params[0] + "%";
Uri uri = Uri.parse("content://" + "AUTHORITY" + "/" + "BASE_DATA_NAME" + "/" + SearchManager.SUGGEST_URI_PATH_QUERY);
String[] projection = {"SELECT", "UPDATE"};
String selection = "SELECT FROM";
String[] selectionArgs = {wildcardQuery, wildcardQuery};
String sortOrder = "ASC";
Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
return cursor;
}
@Override
protected void onPostExecute(Cursor cursor) {
Log.i(TAG, "DEBUG : Entree - onPostExecute");
String[] dataColumns = {
"COLUMN_ID",
"COLUMN_FIRSTNAME"};
int[] viewIDs = {
R.id.list_item_emp_id,
R.id.list_item_name};
String[] items = { "Milk", "Butter", "Yogurt", "Toothpaste", "Ice Cream" };
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getApplication(),
android.R.layout.simple_list_item_1, items);
listView.setAdapter(adapter);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_search:
onSearchRequested();
// break;
return true;
case android.R.id.home:
// This is called when the Home (Up) button is pressed
// in the Action Bar.
Intent parentActivityIntent = new Intent(this, MainSongs4Heaven_p2.class);
parentActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(parentActivityIntent);
finish();
// break;
return true;
default:
return false;
}
// return super.onOptionsItemSelected(item);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.i(TAG, "onItemClick");
finish();
}
}
5) Song class
package songs4heaven_p2.com.hfad.songs4heaven_p2;
public class Song {
Integer _ID;
String _title;
String _words;
String _chords;
String _author;
// constructor
public Song() {
}
// constructor with parameters
public Song(Integer ID, String title, String words, String chord, String author) {
this._ID = ID;
this._title = title;
this._words = words;
this._title = title;
}
// All get methods
public Integer getId() {
return this._ID;
}
public String getTitle() {
return this._title;
}
public String getWords() {
return this._words;
}
public String getChords() {
return this._chords;
}
public String getAuthor() {
return this._author;
}
// All set methods
public void setId(Integer ID) {
this._ID = ID;
}
public void setTitle(String title) {
this._title = title;
}
public void setWords(String words) {
this._words = words;
}
public void setChords(String chords) {
this._chords = chords;
}
public void setAuthor(String author) {
this._author = author;
}
//
@Override
public String toString() {
return _ID + "\n" + _title + " " + _words + "\n" + _chords + "\n" + _author;
}
}
6) Singleton to load my xml file in a list of song objects
package songs4heaven_p2.com.hfad.songs4heaven_p2;
import android.content.Context;
import java.util.List;
public class SingleLoadXml {
private static List<Song> mSong = null;
public static List<Song> getSongs(Context context) {
if ( mSong == null ) {
// je récupère les données XML
SongXmlParser parser = new SongXmlParser();
mSong = parser.parse(context);
}
return mSong;
}
}
7) Main class for the search call
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class MainSongs4Heaven_p2 extends AppCompatActivity {
TextView tv1;
public List<Song> songs = null;
// pour distinguer le démarrage (affichage du chant n°1) et ensuite (chant selon valeur Spinner)
boolean Demarrage = true;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_songs4_heaven_p2);
tv1=(TextView)findViewById(R.id.paroles_chant);
Log.d("DEBUG", "Avant try");
// 20180717 : Appelle XMLPULLPARSER & RETURN A LIST
// SongXmlParser.parse => retourne un songList soit un ArrayList<Song>
// Inspiré de http://innovativenetsolutions.com/2013/02/android-xml-parsing/
// Ce 20180719 : je remplace ces 2 lignes par un Singleton : SingleLoadXml
//SongXmlParser parser = new SongXmlParser();
//songs = parser.parse(getBaseContext());
songs=SingleLoadXml.getSongs( getBaseContext() );
// 201780716 : je change la technique qui allait systématiquement boucler sur le xml
// en utilisant désormais le résultat du parse de SongXmlParser
initSpinnerWithXml();
Log.d("DEBUG", "Juste après initParoles");
}
// 20180729
// https://developer.android.com/training/search/setup
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("DEBUG", "Entrée dans onCreateOptionsMenu");
//MenuInflater inflater = getMenuInflater();
//inflater.inflate(R.menu.options_menu, menu);
getMenuInflater().inflate(R.menu.options_menu, menu);
// Get the SearchView and set the searchable configuration
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
android.support.v7.widget.SearchView searchView = (android.support.v7.widget.SearchView) menu.findItem(R.id.action_search).getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setIconifiedByDefault(false);
Log.i("DEBUG", "before onQueryText*: ");
searchView.setOnQueryTextListener(new android.support.v7.widget.SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
Log.i("DEBUG", "onQueryTextSubmit: ");
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
Log.i("DEBUG", "onQueryTextChange: ");
return false;
}
});
Log.i("DEBUG", "before return true in onCreateOptionsMenu ");
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_search:
onSearchRequested();
return true;
default:
return false;
}
}
Sum up
To sum it up, it seems that I lack something in the URI contents. Here is the result of the debug in the SearchAndroidActivity
Log.i(TAG, "DEBUG : URI:details:"+details.getLastPathSegment());
Log.i(TAG, "DEBUG : URI:details2:"+details2);
Log.i(TAG, "DEBUG : URI:details3:"+details3);
Log.i(TAG, "DEBUG : URI:details full:"+details);
Log.i(TAG, "DEBUG : URI:id:"+id);
Log.i(TAG, "DEBUG : URI:name:"+name);
Log.i(TAG, "DEBUG : resolve:"+resolve);
DEBUG: getAction:android.intent.action.VIEW DEBUG : handleIntent: Intent.ACTION_VIEW DEBUG : URI:details:1 DEBUG : URI:details2:1 DEBUG : URI:details3:ComponentInfo{songs4heaven_p2.com.hfad.songs4heaven_p2/songs4heaven_p2.com.hfad.songs4heaven_p2.SearchAndroidActivity} DEBUG : URI:details full:1 DEBUG : URI:id:null DEBUG : URI:name:null DEBUG : resolve:ComponentInfo{songs4heaven_p2.com.hfad.songs4heaven_p2/songs4heaven_p2.com.hfad.songs4heaven_p2.SearchAndroidActivity}
intent.getDataString() does not return anything else than a numeric. When comparing to another app, I should have something like an URI :
like
content://com.himebaugh.employeedirectory.EmployeeProvider/employees/3
Could you help me ?
Thank you very much.
Jean-michel, Nemours, France