Monthly Archives: February 2015

Activity’s

Activity’s are used to display your applications “screens”. They do this by using context defined here.

Context gives you access to your devices resources, such as the display. Context is given to your first activity (MainActivity) by default. You can have multiple screens in your app, but you must pass the context to them so they can use the display. You pass context to another activity by using an intent. Intent’s pass context, but their main purpose is to send a “message” to the system with a request to perform an action, they can also pass data. There are different levels of requests you can make from very general (@@@a notepad app@@@@@) to a specific app (com.example.mynotepad.MainActivity), or in between, which you will use the most (MySecondScreen.class). Context also goes along for the ride.

Intent intent = new Intent(MainActivity.this,MySecondScreen.class);

But we are getting ahead of ourselves let’s look at just a single screen app, which is very simple and created by default when you make a blank app from the Andoid SDK.

package com.damageinc.bigdogweather;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

@Override
    public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
}
}

This is the logic part of what you need to display a screen on your device.

Notice MainActivity extends Activity. That means you are creating a new Activity Object that is a copy of Activity Object which is defined by Android. Activity has context, which you can not see, by default, ALL Activity’s CAN have Context, it just needs to be passed in either by you or the system, the system only gives it to the first Activity of the applicaiton.

Activity’s have all kinds of method’s in them, and when you extend it you get them for free and are used without you being able to see them. The reason why you usually extend them is because you want them to do their thing for the most part, but you also need to make some custom changes to do what you want. That is what the @Override command is for. @Override grabs the system command that usually calls the default method and let’s you use it first. But you are not going to re-write the whole method over, you just want to change some things about it so you must also “pass it on” to the system. The version the system uses is known as the superclass and is defined by the super.onCreate() line. When we start the app we want it to do what we want, not what it does by default, so we @Override the onCreate() method. In this instance we want to display our own screen. It is defined by a different file (a .xml file) in a different location. This is known as the MVC architecture (Model-View-Controller) architecture, this is something to make programmers sound smart, it basically means that you separate your view and your logic so you can re-use a common view (screen) by copy and pasting it into different applications and then linking the screen objects to the new logic.

We tell the application which screen we want to use with the following command.

setContentView(R.layout.activity_main);

R.layout.activity_main is the path to our file that contains the description of the screen layout we want to use.

R. – is a file in the application (THAT YOU SHOULD NEVER CHANGE) it is essentially a file that points to where all your different resources are in physical memory, so if you open the file (you can do that) you will see the names of your resources and memory addresses (app/gen/com/example/appname/R.java).

layout – is the directory in your app_name/res/layout folder for your application.

activity_main – is the actual file that has the description of the screen you want to display.

This causes your screen to show on your device. (The end brackets close out the method and activity code location)

Now lets look at our activity_main.xml file

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

</LinearLayout>

For now just notice that it is configured to fill the entire screen

android:layout_width=”fill_parent”
android:layout_height=”fill_parent”

It will display a line of text:

Android Filtering

public class FragmentLst extends ListFragment implements OnQueryTextListener
{

	@Override
	public boolean onQueryTextSubmit(String p1)
	{
		// TODO: Implement this method
		return false;
	}

	@Override
	public boolean onQueryTextChange(String p1)
	{
		// TODO: Implement this method
		myListAdapter.getFilter().filter(p1);
		return false;
	}
import android.content.*;
import android.net.*;
import android.view.*;
import android.widget.*;
import java.util.*;
import java.io.*;

public class MusicAdapter extends BaseAdapter implements Filterable
{
	
	public static int SEARCH_ALBUM_TRACK = 0;
	
	Filter musicFilter;
	List<Music>origMusicList = new ArrayList<Music>();
	List<Music> musicList = new ArrayList<Music>();
	//private List<Music> musicList;
	private Context context;

	public MusicAdapter(Context context,List<Music> passedList){
		//super(context,R.layout.img_row_layout ,passedList);
		this.context = context;
		this.musicList = passedList;
		this.origMusicList = passedList;
	}

	@Override
	public int getCount()
	{
		// TODO: Implement this method
		return musicList.size();
	}

	@Override
	public Object getItem(int p1)
	{
		// TODO: Implement this method
		return musicList.get(p1);
	}

	@Override
	public long getItemId(int p1)
	{
		// TODO: Implement this method
		return 0;
	}

	@Override
	public View getView(int p1, View p2, ViewGroup p3)
	{
		MusicHolder holder = new MusicHolder();
		// TODO: Implement this method
		if(p2 == null){
			LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
			p2 = inflater.inflate(R.layout.img_row_layout,p3,false);
		
			holder.songNameView = (TextView)p2.findViewById(R.id.name);
			holder.albumNameView = (TextView)p2.findViewById(R.id.dist);
			holder.imageView = (ImageView)p2.findViewById(R.id.img);
		//TextView myCover = (TextView)p2.findViewById(R.id.text4);
		//TextView myTime = (TextView)p2.findViewById(R.id.text5);
		
		//holder.songNameView = myName;
		//holder.albumNameView = myAlbum;
		//holder.imageView = myPic;
		
		p2.setTag(holder);
		} else
		holder = (MusicHolder) p2.getTag();
		
		
		
		Music item = musicList.get(p1);
		holder.songNameView.setText(item.name);
		holder.albumNameView.setText(item.album);
		//mySong.setImageURI(Uri.parse("file:/sdcard/buttonArt/killers"));
		holder.imageView.setImageURI(Uri.parse("file:/sdcard/buttonArt/" + (item.coverArt)));
		
		return p2;
		
	}
	
	private static class MusicHolder {
		public TextView songNameView;
		public TextView albumNameView;
		public ImageView imageView;
	}
	
	@Override
	public Filter getFilter() {
		if (musicFilter == null)
			musicFilter = new PlanetFilter();

		return musicFilter;
	}
	
	public void resetData() {
		musicList = origMusicList;
    }



	private class PlanetFilter extends Filter {

		@Override
		protected FilterResults performFiltering(CharSequence constraint) {
			FilterResults results = new FilterResults();
			// We implement here the filter logic
			if (constraint == null || constraint.length() == 0) {
				// No filter implemented we return all the list
				results.values = origMusicList;
				results.count = origMusicList.size();
			}
			else {
				// We perform filtering operation
				List<Music> nPlanetList = new ArrayList<Music>();
				//Only used for Filtering not with Search Widget
				for (Music p : musicList) {
					if(SEARCH_ALBUM_TRACK == 1){
					if (p.name.toUpperCase().startsWith(constraint.toString().toUpperCase()))
						nPlanetList.add(p);
						} else {
					if (p.album.toUpperCase().startsWith(constraint.toString().toUpperCase()))
						nPlanetList.add(p);
					}

				}

				results.values = nPlanetList;
				results.count = nPlanetList.size();

			}
			return results;
		}

		@Override
		protected void publishResults(CharSequence constraint,
									  FilterResults results) {

			// Now we have to inform the adapter about the new list filtered
			if (results.count == 0)
				notifyDataSetInvalidated();
			else {
				musicList = (List<Music>) results.values;
				notifyDataSetChanged();
			}

		}
	}
	
	
}

Change the searchable.xml file back to this:

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
	android:label="@string/search_label"
	android:hint="@string/search_hint"
	>
</searchable>

Android Content Provider – full blown

This version contains all the extra’s Google recommends:

**** fix paths

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.net.Uri;
import android.provider.BaseColumns;

public class MusicProvider extends ContentProvider {

    public static String AUTHORITY = "com.example.blogfinal5.MusicProvider";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/discography");

    // MIME types used for searching words or looking up a single definition
    public static final String SONGS_MIME_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE +
	"/vnd.example.blogfinal5";
    public static final String ALBUM_MIME_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE +
	"/vnd.example.blogfinal5";
	//----------> MusicDatabase initializer <------
	//*****See onCreate method
    private MusicDatabase mDiscography;

    // UriMatcher stuff
    private static final int SEARCH_SONGS = 0;
    private static final int GET_SONG = 1;
    private static final int SEARCH_SUGGEST = 2;
    private static final int REFRESH_SHORTCUT = 3;
    private static final UriMatcher sURIMatcher = buildUriMatcher();

    /**
     * Builds up a UriMatcher for search suggestion and shortcut refresh queries.
     */
    private static UriMatcher buildUriMatcher() {
        UriMatcher matcher =  new UriMatcher(UriMatcher.NO_MATCH);
        // to get definitions...
        matcher.addURI(AUTHORITY, "discography", SEARCH_SONGS);
        matcher.addURI(AUTHORITY, "discography/#", GET_SONG);
        // to get suggestions...<<also used to populate suggestion list>>
        matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
        matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);

        /* The following are unused in this implementation, but if we include
         * {@link SearchManager#SUGGEST_COLUMN_SHORTCUT_ID} as a column in our suggestions table, we
         * could expect to receive refresh queries when a shortcutted suggestion is displayed in
         * Quick Search Box, in which case, the following Uris would be provided and we
         * would return a cursor with a single item representing the refreshed suggestion data.
         */
        matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT, REFRESH_SHORTCUT);
        matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", REFRESH_SHORTCUT);
        return matcher;
    }

    @Override
    public boolean onCreate() {
        mDiscography = new MusicDatabase(getContext());
        return true;
    }

    /**
     * Handles all the dictionary searches and suggestion queries from the Search Manager.
     * When requesting a specific word, the uri alone is required.
     * When searching all of the dictionary for matches, the selectionArgs argument must carry
     * the search query as the first element.
     * All other arguments are ignored.
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
                        String sortOrder) {

        // Use the UriMatcher to see what kind of query we have and format the db query accordingly
        switch (sURIMatcher.match(uri)) {
            case SEARCH_SUGGEST:
                if (selectionArgs == null) {
					throw new IllegalArgumentException(
						"selectionArgs must be provided for the Uri: " + uri);
                }
                return getSuggestions(selectionArgs[0]);
            case SEARCH_SONGS:
                if (selectionArgs == null) {
					throw new IllegalArgumentException(
						"selectionArgs must be provided for the Uri: " + uri);
                }
                return search(selectionArgs[0]);
            case GET_SONG:
                return getSong(uri);
            case REFRESH_SHORTCUT:
                return refreshShortcut(uri);
            default:
                throw new IllegalArgumentException("Unknown Uri: " + uri);
        }
    }
    //This is used to create Suggestion dropdown list. Only displays 2 items  ????
    private Cursor getSuggestions(String query) {
		query = query.toLowerCase();
		String[] columns = new String[] {
			BaseColumns._ID,
			MusicDatabase.KEY_SONG,
			MusicDatabase.KEY_ALBUM,
			SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID};

		return mDiscography.getSongMatches(query, columns);
    }
    //This is used to CREATE the list displayed when you click the magnifing glass / enter
    private Cursor search(String query) {
		query = query.toLowerCase();
		String[] columns = new String[] {
			BaseColumns._ID,
			MusicDatabase.KEY_SONG,
			MusicDatabase.KEY_ALBUM,
			MusicDatabase.KEY_DB_NUM};

		return mDiscography.getSongMatches(query, columns);
    }
    //This is used to display the final screen (DisplayActivity.class) when you select item from Suggestion DropDown 
    //or List created from Clicking the Magnifying Glass / Enter key
    private Cursor getSong(Uri uri) {
		String rowId = uri.getLastPathSegment();
		String[] columns = new String[] {
			MusicDatabase.KEY_SONG,
			MusicDatabase.KEY_ALBUM,
			MusicDatabase.KEY_DB_NUM,
			MusicDatabase.KEY_COVER_ART,
			MusicDatabase.KEY_TIME};

		return mDiscography.getSong(rowId, columns);
    }

    private Cursor refreshShortcut(Uri uri) {
		/* This won't be called with the current implementation, but if we include
		 * {@link SearchManager#SUGGEST_COLUMN_SHORTCUT_ID} as a column in our suggestions table, we
		 * could expect to receive refresh queries when a shortcutted suggestion is displayed in
		 * Quick Search Box. In which case, this method will query the table for the specific
		 * word, using the given item Uri and provide all the columns originally provided with the
		 * suggestion query.
		 */
		String rowId = uri.getLastPathSegment();
		String[] columns = new String[] {
			BaseColumns._ID,
			MusicDatabase.KEY_SONG,
			MusicDatabase.KEY_ALBUM,
			SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
			SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID};

		return mDiscography.getSong(rowId, columns);
    }

    /**
     * This method is required in order to query the supported types.
     * It's also useful in our own query() method to determine the type of Uri received.
     */
    @Override
    public String getType(Uri uri) {
        switch (sURIMatcher.match(uri)) {
            case SEARCH_SONGS:
                return SONGS_MIME_TYPE;
            case GET_SONG:
                return ALBUM_MIME_TYPE;
            case SEARCH_SUGGEST:
                return SearchManager.SUGGEST_MIME_TYPE;
            case REFRESH_SHORTCUT:
                return SearchManager.SHORTCUT_MIME_TYPE;
            default:
                throw new IllegalArgumentException("Unknown URL " + uri);
        }
    }

    // Other required implementations...

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }

}

Android – Casting

Casting

(TextView)

(LayoutInflater)

(String)

(Button)

(int)

TextView readThis = (TextView) findViewById(R.id.read_this);

Button press = (Button) findViewById(R.id.button1);

SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);

next is Casting – ***Very Big*** in Android programming. So you are asking for a Object , you know what Object it is and what it looks like, but the OS is un-aware so you have to tell it what the Object is, and once you tell it what it is it knows how to handle it.

Android – Constructor

Constructor

When you create an instance of this class, i.e. instantiate it, this is what you call to create the Object. The class is actually just blue prints on how to make the Object and when you call the constructor you actually create the Object. You can have more than one constructor, but they must be unique. If you have ever run command line’s in dos or any other non-GUI operating systems this is very similar(dir /s, dir /a, dir /o these would all use the same constructor type), but again the parameter types you pass in must be different to require you to create a different constructor.

For example:

public MusicAdapter (Context context, List musicList){}

public MusicAdapter (Context context, String song){}

public MusicAdapter (Context context, boolean well){}

you can have all these constructors in the same class, but you can not add:

public MusicAdapter (Context context, List songList){}

this has already been taken.

Android – Context

Context

Context – Wow it took me a long time to wrap my head around this one, it is pretty simple, not that I was saying that 15 minutes ago…… people just over explain it to death.

Technical: Context is a class that has access to system resources.

Non-Technical: Context is the owner of CostCo.

Technical: Apps have a subset of system resources.

Non-Technical: A CostCo store has a subset of their resources.

Technical: Activity’s inherit from Context.

Non-technical: You are CostCo’s owners’ son/daughter you inherit from him, you are an Activity.

Non-technical: You inherit a card that allows you to have ANYTHING (resources) you want from one store. The card is context, you give the card(this) to your friend (can I be your friend?) he is set, he get’s ANYTHING (resources) he wants from that one store. The card only let’s you go to that one store.

Technical: Context gives access to your applications resources to your MainActivity class. Your MainActivity class can pass a token/card(this) to other classes so they have access to your resources(like R.layout.main). Your context only gives you access to your apps resources.

Done.

I may expand on this in a more technical manner later, but for now, get this horse outta here!

One last thing, you are sooooo nice (you’re still rich right?) you don’t even have to give your friends the card they can just call you (getActivity()) and you will bring it to them (.getApplicationContext()).
Used together it looks like this of course getActivity().getApplicationContext()

Android – Object

An Object is a thing with characteristics and essentially allows you to say in your application I want this one thing and you get all the characteristics along with it for free. For instance in this app when you select a song on the list you not only get the song, you get the album, the cover art, the number that actually represents the song in the database on the iPhone / iPod Touch (Uri), and the song length.

I LOVE analogies so here is one for you:

A car is like an Object, when you buy one, you ask for a car and they give you the car, the transmission, the tires, the brakes, the wheels, the gas tank which are all characteristics of a car. I would say for free, but we know that’s not the case, and we know that every day they try more and more to make a car not an Object….”Oh, you wanted a transmission? That’s extra.”

Also see: Casting

Android Search with FTS3

Now we are going to add suggestions to our mp3 app.

First we need to convert our data to a SQLite FTS3 (full-text search) database.

Search will need us to turn our .txt file into a SQLite Database so we can perform FTS3 (full-text search) which is like a Like query, but more robust, and can produce results much faster.

**** Overview of what you are about to see.

First time through we will perform our query, which in our case means perform a query using a ContentProvider.

This will take us to the Manifest file which will point us to our ContentProvider which we will have in a separate file containing our ContentProvider class(MusicProvider.java).

When we create our ContentProvider MusicProvider class we will instantiate a database class in another file(MusicDatabase.java).

When we instantiate the MusicDatabase it will CREATE A VIRTUAL TABLE USING FTS.

Then it will read through each line of our .txt file and .add() each line to the database.

Then it will perform a query on the database.(If you have 4000 songs the database load can take a couple minutes, so don’t be alarmed if you don’t find a song or album beginning with Z on your very first query) It will actually look like the app has hung with that many songs so you might want to check your logic with a smaller list. I have checked this code and it did work, the screen was just white for a couple minutes.

Don’t forget to change your path statements if needed.

android:authorities=”com.example.blogmp3final.MusicProvider”

You may have noticed that if you do several searches the app will open several instances. So when you try to go back to exit the app you have to close the app several times.

By adding:
android:launchMode=”singleTop”

You can keep this from happening, but you must add functionality to the MainActivity.java class.

<activity
            android:label="@string/app_name"
            android:name=".MainActivity"
			android:launchMode="singleTop">
            <intent-filter >
. . .

        </activity>
        <provider android:name=".MusicProvider"
            android:authorities="com.example.blogmp3final.MusicProvider" />
    </application>
MainActivity.java

@Override
    protected void onNewIntent(Intent intent) {
        // Because this activity has set launchMode="singleTop", the system calls this method
        // to deliver the intent if this activity is currently the foreground activity when
        // invoked again (when the user executes a search from this activity, we don't create
        // a new instance of this activity, so the system delivers the search intent here)
        handleIntent(intent);
    }
MainActivity.java
private void handleIntent(Intent intent){
		//ACTION_VIEW handles clicks on suggestions
		//ACTION_SEARCH is when you click on the keyboard
		 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
			//Toast.makeText(this,"Selected",Toast.LENGTH_SHORT).show();

			Uri uri = intent.getData();
			
			Cursor cursor = getContentResolver().query(uri,null,null,null,null);

			if(cursor == null){
				finish();
			} else {
				cursor.moveToFirst();
				//db representation for song
				int sIndex = cursor.getColumnIndexOrThrow(MusicDatabase.KEY_DB_NUM);
				JSONObject jsonData = new JSONObject();

				try {
					jsonData.put("request", REQUEST_TOAST_ME);
					jsonData.put("song", cursor.getString(sIndex));
				} catch (JSONException e) {
					e.printStackTrace();
					Log.e(TAG, "can't put request");
					return;
				}

				//showToast(item.song);
				new  SocketServerTask().execute(jsonData);
				
			}
		}
	}

Create a new class / file

Change the paths if necessary.

MusicProvider.java

import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
  
public class MusicProvider extends ContentProvider {
     
    public static String AUTHORITY = "com.example.blogmp3final.MusicProvider";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/discography");
  
    //----------> MusicDatabase initializer <------
    //*****See onCreate method
    private MusicDatabase mDiscography;
  
    // UriMatcher stuff
    private static final int SEARCH_SONGS = 0;
    private static final int GET_SONG = 1;
    private static final int SEARCH_SUGGEST = 2;
   private static final UriMatcher sURIMatcher = buildUriMatcher();
  
    /**
     * Builds up a UriMatcher for search suggestion and shortcut refresh queries.
     */
    private static UriMatcher buildUriMatcher() {
        UriMatcher matcher =  new UriMatcher(UriMatcher.NO_MATCH);
        // to get definitions...
        matcher.addURI(AUTHORITY, "discography", SEARCH_SONGS);
        matcher.addURI(AUTHORITY, "discography/#", GET_SONG);
        // to get suggestions...<<also used to populate suggestion list>>
        matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
        matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
 
        return matcher;
    }
  
    @Override
    public boolean onCreate() {
        mDiscography = new MusicDatabase(getContext());
        return true;
    }
 
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
                        String sortOrder) {
  
        String query;
        String [] columns;
        // Use the UriMatcher to see what kind of query we have and format the db query accordingly
        switch (sURIMatcher.match(uri)) {
             
            case SEARCH_SUGGEST:
                if (selectionArgs == null) {
                    throw new IllegalArgumentException(
                        "selectionArgs must be provided for the Uri: " + uri);
                }
                query = selectionArgs[0].toLowerCase();
                columns = new String[] {
                    BaseColumns._ID,
                    MusicDatabase.KEY_SONG,
                    MusicDatabase.KEY_ALBUM,
                    SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID};
          
                return mDiscography.getSongMatches(query, columns);
                 
            case SEARCH_SONGS:
                if (selectionArgs == null) {
                    throw new IllegalArgumentException(
                        "selectionArgs must be provided for the Uri: " + uri);
                }
                query = selectionArgs[0].toLowerCase();
                columns = new String[] {
                    BaseColumns._ID,
                    MusicDatabase.KEY_SONG,
                    MusicDatabase.KEY_ALBUM,
                    MusicDatabase.KEY_DB_NUM};
          
                return mDiscography.getSongMatches(query, columns);
            case GET_SONG:
                String rowId = uri.getLastPathSegment();
                columns = new String[] {
                    MusicDatabase.KEY_SONG,
                    MusicDatabase.KEY_ALBUM,
                    MusicDatabase.KEY_DB_NUM,
                    MusicDatabase.KEY_COVER_ART,
                    MusicDatabase.KEY_TIME};
          
                return mDiscography.getSong(rowId, columns);
                 
            default:
                throw new IllegalArgumentException("Unknown Uri: " + uri);
        }
    }
 
 
    @Override
    public String getType(Uri uri) {
        return "OMG";
    }
  
    // Other required implementations...
  
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException();
    }
  
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }
  
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }
  
}

You owe me, for the code above, I must have cut the lines of code in half, got rid of a lot of worthless stuff, like seat belts, air bags, anti-lock breaks, power steering…

What we have above is just the bare minimum of what we need for this app to work.
If you want to see the whole thing go here.

Create a new class / file

MusicDatabase.java

import android.app.*;
import android.content.*;
import android.content.res.*;
import android.database.*;
import android.database.sqlite.*;
import android.os.*;
import android.provider.*;
import android.text.*;
import android.util.*;
import java.io.*;
import java.util.*;
 
public class MusicDatabase{
 
    //The columns we'll include in the discography table
    public static final String KEY_SONG = SearchManager.SUGGEST_COLUMN_TEXT_1;
    public static final String KEY_ALBUM = SearchManager.SUGGEST_COLUMN_TEXT_2;
    public static final String KEY_DB_NUM = "db_num";
    public static final String KEY_COVER_ART = "cover_art";
    public static final String KEY_TIME = "length_time";
 
    private static final String DATABASE_NAME = "discography";
    private static final String FTS_VIRTUAL_TABLE = "FTSdiscography";
    private static final int DATABASE_VERSION = 2;
 
    private final DiscographyOpenHelper mDatabaseOpenHelper;
    private static final HashMap<String,String> mColumnMap = buildColumnMap();
 
 
    /**
     * Constructor
     * @param context The Context within which to work, used to create the DB
     */
    public MusicDatabase(Context context) {
        mDatabaseOpenHelper = new DiscographyOpenHelper(context);
    }
 
    /**
     * Builds a map for all columns that may be requested, which will be given to the 
     * SQLiteQueryBuilder. This is a good way to define aliases for column names, but must include 
     * all columns, even if the value is the key. This allows the ContentProvider to request
     * columns w/o the need to know real column names and create the alias itself.
     */
    private static HashMap<String,String> buildColumnMap() {
        HashMap<String,String> map = new HashMap<String,String>();
        map.put(KEY_SONG, KEY_SONG);
        map.put(KEY_ALBUM, KEY_ALBUM);
        map.put(KEY_DB_NUM, KEY_DB_NUM);
        map.put(KEY_COVER_ART, KEY_COVER_ART);
        map.put(KEY_TIME, KEY_TIME);
        map.put(BaseColumns._ID, "rowid AS " +
                BaseColumns._ID);
        map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " +
                SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
        map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "rowid AS " +
                SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
        return map;
    }
 
    /**
     * Returns a Cursor positioned at the word specified by rowId
     *
     * @param rowId id of song to retrieve
     * @param columns The columns to include, if null then all are included
     * @return Cursor positioned to matching song, or null if not found.
     */
    public Cursor getSong(String rowId, String[] columns) {
        String selection = "rowid = ?";
        String[] selectionArgs = new String[] {rowId};
 
        return query(selection, selectionArgs, columns);
 
        /* This builds a query that looks like:
         *     SELECT <columns> FROM <table> WHERE rowid = <rowId>
         */
    }
 
    /**
     * Returns a Cursor over all songs that match the given query
     *
     * @param query The string to search for
     * @param columns The columns to include, if null then all are included
     * @return Cursor over all songs that match, or null if none found.
     */
    public Cursor getSongMatches(String query, String[] columns) {
        String selection = KEY_ALBUM + " LIKE ?";
        String[] selectionArgs = new String[] {"%"+query+"%"};
 
        return query(selection, selectionArgs, columns);
 
        /* This builds a query that looks like:
         *     SELECT <columns> FROM <table> WHERE <KEY_SONG> MATCH 'query*'
         * which is an FTS3 search for the query text (plus a wildcard) inside the word column.
         *
         * - "rowid" is the unique id for all rows but we need this value for the "_id" column in
         *    order for the Adapters to work, so the columns need to make "_id" an alias for "rowid"
         * - "rowid" also needs to be used by the SUGGEST_COLUMN_INTENT_DATA alias in order
         *   for suggestions to carry the proper intent data.
         *   These aliases are defined in the DictionaryProvider when queries are made.
         * - This can be revised to also search the definition text with FTS3 by changing
         *   the selection clause to use FTS_VIRTUAL_TABLE instead of KEY_WORD (to search across
         *   the entire table, but sorting the relevance could be difficult.
         */
    }
 
    /**
     * Performs a database query.
     * @param selection The selection clause
     * @param selectionArgs Selection arguments for "?" components in the selection
     * @param columns The columns to return
     * @return A Cursor over all rows matching the query
     */
    private Cursor query(String selection, String[] selectionArgs, String[] columns) {
        /* The SQLiteBuilder provides a map for all possible columns requested to
         * actual columns in the database, creating a simple column alias mechanism
         * by which the ContentProvider does not need to know the real column names
         */
        SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
        builder.setTables(FTS_VIRTUAL_TABLE);
        builder.setProjectionMap(mColumnMap);
 
        Cursor cursor = builder.query(mDatabaseOpenHelper.getReadableDatabase(),
                                      columns, selection, selectionArgs, null, null, null);
 
        if (cursor == null) {
            return null;
        } else if (!cursor.moveToFirst()) {
            cursor.close();
            return null;
        }
        return cursor;
    }
 
 
    /**
     * This creates/opens the database.
     */
    private static class DiscographyOpenHelper extends SQLiteOpenHelper {
 
        private final Context mHelperContext;
        private SQLiteDatabase mDatabase;
 
        /* Note that FTS3 does not support column constraints and thus, you cannot
         * declare a primary key. However, "rowid" is automatically used as a unique
         * identifier, so when making requests, we will use "_id" as an alias for "rowid"
         */
        private static final String FTS_TABLE_CREATE =
        "CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE +
        " USING fts3 (" +
        KEY_SONG + ", " +
        KEY_ALBUM + ", " +
        KEY_DB_NUM + ", " +
        KEY_COVER_ART + ", " +
        KEY_TIME + ");";
 
        DiscographyOpenHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
            mHelperContext = context;
        }
 
        @Override
        public void onCreate(SQLiteDatabase db) {
            mDatabase = db;
            mDatabase.execSQL(FTS_TABLE_CREATE);
            loadDiscography();
        }
 
        /**
         * Starts a thread to load the database table with song
         */
        private void loadDiscography() {
            new Thread(new Runnable() {
                    public void run() {
                        try {
                            loadSongs();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }).start();
        }
 
        private void loadSongs() throws IOException {
            //Log.d(TAG, "Loading songs...");
            File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
 
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File(path,"ohyeah3.txt"))));
 
            try {
                String line;
                while ((line = reader.readLine()) != null) {
                    String[] strings = TextUtils.split(line, ",");
                    if (strings.length < 5) continue;
                    long id = addSong(strings[0].trim(), strings[1].trim(), strings[2].trim(), strings[3].trim(), strings[4].trim());
                    if (id < 0) {
                        //Log.e(TAG, "unable to add song: " + strings[0].trim());
                    }
                }
            } finally {
                reader.close();
            }
            //Log.d(TAG, "DONE loading songs.");
        }
 
        /**
         * Add a word to the dictionary.
         * @return rowId or -1 if failed
         */
        public long addSong(String song, String album, String dbnum, String coverart, String dbtime) {
            ContentValues initialValues = new ContentValues();
            initialValues.put(KEY_SONG, song);
            initialValues.put(KEY_ALBUM, album);
            initialValues.put(KEY_DB_NUM, dbnum);
            initialValues.put(KEY_COVER_ART, coverart);
            initialValues.put(KEY_TIME, dbtime);
 
            return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues);
        }
 
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            //Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
              //    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE);
            onCreate(db);
        }
    }
 
}

Update searchable.xml file (fix paths if needed)

searchable.xml
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android" 

	android:label="@string/search_label"
	android:hint="@string/search_hint"
	android:searchSettingsDescription="@string/settings_description"
	android:searchSuggestAuthority="com.example.blogmp3final.MusicProvider"
	android:searchSuggestIntentAction="android.intent.action.VIEW"
	android:searchSuggestIntentData="content://com.example.blogmp3final.MusicProvider/discography"
	android:searchSuggestSelection=" ?"
	android:searchSuggestThreshold="1"
	android:includeInGlobalSearch="true">
</searchable>

**** I jumped around a little bit after this post, go to the Table Of Contents if you want to continue on with this tutorial.

Android Search – Part 2

**** Project Status – Code Verified

In this post we will remove all the noise of our mp3 app and just make a simple ListView that uses search.

We left off on the previous post ready to look at our handleIntent() method which catches the Intent sent by our SearchView Widget when we type text into it.

Don’t forget we need what we went over in the previous post to make this work.

import android.app.*;
import android.content.*;
import android.os.*;
import android.view.*;
import android.widget.*;
import java.io.*;
import java.util.*;
 
public class MainActivity extends ListActivity
{
    File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
    String buff;
    String[] rawMusicInfo;
    BufferedReader myBufferReader;
    List<Music> musicList = new ArrayList<Music>();
    List<String>sitems = new ArrayList<String>();
     
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
         
        try{
            myBufferReader = new BufferedReader(new InputStreamReader(new FileInputStream(new File(path,"ohyeah3.txt"))));
        } catch (FileNotFoundException e){}
 
     
        StringBuilder holdThis = new StringBuilder();
        if(myBufferReader != null){
            try{
                while((buff = myBufferReader.readLine()) != null){
                    buff = buff.replace(System.getProperty("line.separator"),"");
                    holdThis.append(buff);
                }
            } catch(IOException e){
                            e.printStackTrace();
}
        }
         
        rawMusicInfo = holdThis.toString().split(",");
         
        for(int i = 0;i < rawMusicInfo.length;i=i+5){
            //List of song titles to pass to ListView           
            sitems.add(rawMusicInfo[i+1]);
            
            Music entry = new Music();
            entry.album = rawMusicInfo[i];
            entry.name = rawMusicInfo[i+1];
            entry.song = rawMusicInfo[i+2];
            entry.coverArt = rawMusicInfo[i+3];
            entry.time = rawMusicInfo[i+4];
            //List for displaying any music characteristic we wish
            musicList.add(entry);
        }
         
        setListAdapter(new ArrayAdapter<String>(MainActivity.this,
                                                android.R.layout.simple_list_item_1, sitems));
        handleIntent(getIntent());
    }
       
   public class Music {
       private String name;
       private String album;
       private String song;
       private String coverArt;
       private String time;
   }
     
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
 
        		if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
            SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
            MenuItem searchItem = menu.findItem(R.id.search);
            SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
            searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
            searchView.setIconifiedByDefault(false);
        }
		return true;
    }
 
    private void handleIntent(Intent intent){
    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
        // handles a search query
        //Toast.makeText(this,"Querying...",Toast.LENGTH_LONG).show();
        String query = intent.getStringExtra(SearchManager.QUERY);
        //showResults(query);
        sitems.clear();
         
        for(int i = 0;i < musicList.size();i++){
            Music item = musicList.get(i);
            if(item.name.contains(query)){
                sitems.add(item.name);
            }
        }
                             
        ((ArrayAdapter) getListAdapter()).notifyDataSetChanged();
    }
    }
    
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        // TODO Auto-generated method stub
        super.onListItemClick(l, v, position, id);
        //Music music = (Music)getListAdapter().getItem(position);
        Toast.makeText(MainActivity.this,"You Clicked Cell " + position,Toast.LENGTH_SHORT).show();
        Toast.makeText(MainActivity.this,"You Clicked " + sitems.get(position),Toast.LENGTH_SHORT).show(); 
    }
}

This gives us everything we need except our Manifest file permissions and our .txt file in our Downloads directory.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

Text file contents I will post at the bottom.

So in this app we are going to use really slow search logic, but it will do the job.

We will receive the search Intent and extract the action, there are two kinds of actions that search uses ACTION_SEARCH and ACTION_VIEW.

ACTION_SEARCH – Is sent when you press the magnifying glass on the keyboard.
ACTION_VIEW – Is sent when you use suggestions and a character is typed in the SearchView Widget.

We are using just the return key so we check for ACTION_SEARCH.

if (Intent.ACTION_SEARCH.equals(intent.getAction())) {

intent.getAction() extracts the action type from the intent that was passed in.

and we compare it to the static variable in the Intent class (Intent.ACTION_SEARCH) and if they match we extract our search criteria from the intent, which is a static variable in the SearchManager class.

String query = intent.getStringExtra(SearchManager.QUERY);

Once we have the search text saved as a string we clear out our Mutable List of song titles from the last search “sitems”, if there was one.

Then we for loop through each Music Object and check to see if it’s name characteristic contains our search text (query).

If the name contains the search text we add it to the Mutable List of song titles we cleared out before the search.

Then we get our List adapter, which is an ArrayAdapter Object.

(ArrayAdapter) getListAdapter()

and tell it to re-populate by calling an ArrayAdapter method

.notifyDataSetChanged();

**** Review for the rest of the app for those of you just joining.

First we are implementing the ListActivity class that gives us a ListView automatically so we don’t have to create one in our main.xml file in the app_name/res/layout folder.

public class MainActivity extends ListActivity {

Line 09 we create a variable for a List of type Music.

A List is like an array, but again I’m going to jump into iOS lingo, they are Mutable this means they are dynamic and can grow as needed, arrays are static, when you create them they are set to a certain size and remain that size.

Although a List seems like the only way to go, other operations don’t like Lists because they ARE dynamic so most of the time once you have the List the way you want it you will have to convert it to an array to use the data in it.

public class Music {
	String name;
	String album;
	String song;
	String coverArt;
	String time;	
}

And that is all for the Music Object.

Oh the data, you can make a text file and copy this data into it or make your own if you wish.

Call it ohyeah3.txt or change the code accordingly.

Joe Satriani,If,4160282723214640264,Joe Satriani.jpg,00:04:49,
Joe Satriani,Home,4160282723214640269,Joe Satriani.jpg,00:03:25,
Joe Satriani,Luminous Flesh Giants,4160282723214640266,Joe Satriani.jpg,00:05:55,
Joe Satriani,Down Down Down,4160282723214640265,Joe Satriani.jpg,00:06:13,
Joe Satriani,Look My Way,4160282723214640268,Joe Satriani.jpg,00:04:01,
Joe Satriani,Killer Bee Bop,4160282723214640271,Joe Satriani.jpg,00:03:48,
Joe Satriani,S.M.F,4160282723214640267,Joe Satriani.jpg,00:06:42,
Joe Satriani,Sittin' 'Round,4160282723214640274,Joe Satriani.jpg,00:03:38,
Joe Satriani,Slow Down Blues,4160282723214640272,Joe Satriani.jpg,00:07:22,
Joe Satriani,(You're) My World,4160282723214640273,Joe Satriani.jpg,00:03:56,
Joe Satriani,Moroccan Sunset,4160282723214640270,Joe Satriani.jpg,00:04:21,
Joe Satriani,Cool #9,4160282723214640263,Joe Satriani.jpg,00:06:00,
The Extremist,Rubina's Blue Sky Happiness,4160282723214640247,The Extremist.jpg,00:06:09,
The Extremist,Friends,4160282723214640243,The Extremist.jpg,00:03:29,
The Extremist,The Extremist,4160282723214640244,The Extremist.jpg,00:03:43,
The Extremist,Summer Song,4160282723214640248,The Extremist.jpg,00:04:58,
The Extremist,War,4160282723214640245,The Extremist.jpg,00:05:45,
The Extremist,Motorcycle Driver,4160282723214640251,The Extremist.jpg,00:04:56,
The Extremist,Why,4160282723214640250,The Extremist.jpg,00:04:45,
The Extremist,Cryin',4160282723214640246,The Extremist.jpg,00:05:42,
The Extremist,Tears In The Rain,4160282723214640249,The Extremist.jpg,00:01:21,
The Extremist,New Blues,4160282723214640252,The Extremist.jpg,00:06:56,
Flying In a Blue Dream,I Believe / Interview,4160282723214640255,Flying In a Blue Dream.jpg,00:05:52,
Flying In a Blue Dream,The Forgotten Pt. 2,4160282723214640257,Flying In a Blue Dream.jpg,00:05:07,
Flying In a Blue Dream,Into the Light,4160282723214640259,Flying In a Blue Dream.jpg,00:02:29,
Flying In a Blue Dream,The Mystical Potato Head Groove Thing,4160282723214640254,Flying In a Blue Dream.jpg,00:05:09,
Flying In a Blue Dream,The Forgotten Pt. 1,4160282723214640256,Flying In a Blue Dream.jpg,00:01:12,
Flying In a Blue Dream,The Bells of Lal Pt. Two,4160282723214640258,Flying In a Blue Dream.jpg,00:04:06,
Flying In a Blue Dream,Flying in a Blue Dream,4160282723214640253,Flying In a Blue Dream.jpg,00:05:23,
Flying In a Blue Dream,Strange,6292029424235847124,Flying In a Blue Dream.jpg,00:05:02,
Flying In a Blue Dream,I Believe / Interview,6292029424235847125,Flying In a Blue Dream.jpg,00:05:55,
Flying In a Blue Dream,One Big Rush,6292029424235847126,Flying In a Blue Dream.jpg,00:03:26,
Flying In a Blue Dream,The Phone Call,6292029424235847127,Flying In a Blue Dream.jpg,00:03:01,
Flying In a Blue Dream,Day At the Beach (New Rays from an Ancient Sun),7060199914264384165,Flying In a Blue Dream.jpg,00:02:04,
Flying In a Blue Dream,Back to Shalla-Bal,7060199914264384166,Flying In a Blue Dream.jpg,00:03:14,
Flying In a Blue Dream,Ride,7060199914264384167,Flying In a Blue Dream.jpg,00:04:56,
Flying In a Blue Dream,The Forgotten Pt. One,7060199914264384168,Flying In a Blue Dream.jpg,00:01:12,
Flying In a Blue Dream,The Forgotten Pt. Two,7060199914264384169,Flying In a Blue Dream.jpg,00:05:07,
Flying In a Blue Dream,The Mystical Potato Head Groove Thing,8317032066007941762,Flying In a Blue Dream.jpg,00:05:10,
Flying In a Blue Dream,Can't Slow Down,8317032066007941763,Flying In a Blue Dream.jpg,00:04:50,
Flying In a Blue Dream,Headless,8317032066007941764,Flying In a Blue Dream.jpg,00:01:29,
Flying In a Blue Dream,The Bells of Lal Pt. One,8584424278683016416,Flying In a Blue Dream.jpg,00:01:19,
Flying In a Blue Dream,The Bells of Lal Pt. Two,8584424278683016417,Flying In a Blue Dream.jpg,00:04:07,
Flying In a Blue Dream,Into the Light,8584424278683016418,Flying In a Blue Dream.jpg,00:02:30,
Flying In a Blue Dream,The Feeling / Interview,1148073617125400889,Flying In a Blue Dream.jpg,00:00:50,
Flying In a Blue Dream,Big Bad Moon / Interview,1148073617125400875,Flying In a Blue Dream.jpg,00:05:15,
Flying In a Blue Dream,Flying In a Blue Dream,1148073617125400888,Flying In a Blue Dream.jpg,00:05:23,
Not Of This Earth,Not Of This Earth,4160282723214640312,Not Of This Earth.jpg,00:04:00,
Not Of This Earth,The Snake,4160282723214640313,Not Of This Earth.jpg,00:04:41,
Not Of This Earth,Driving At Night,4160282723214640318,Not Of This Earth.jpg,00:03:31,
Not Of This Earth,The Headless Horseman,4160282723214640321,Not Of This Earth.jpg,00:01:53,
Not Of This Earth,Hordes Of Locusts,4160282723214640319,Not Of This Earth.jpg,00:04:56,
Not Of This Earth,New Day,4160282723214640320,Not Of This Earth.jpg,00:03:46,
Not Of This Earth,Rubina,4160282723214640314,Not Of This Earth.jpg,00:05:53,
Not Of This Earth,Memories,4160282723214640315,Not Of This Earth.jpg,00:04:04,
Not Of This Earth,The Enigmatic,4160282723214640317,Not Of This Earth.jpg,00:03:26,
Not Of This Earth,Brother John,4160282723214640316,Not Of This Earth.jpg,00:02:08