[DEPRECATED] APIs used to create extensions for Blur Launcher
Java
Latest commit 222672e Aug 26, 2016 @klinker24 committed on GitHub Update README.md

README.md

Blur Launcher API's

Deprecation Notice

These APIs will no longer apply to Blur. With Android 7.0, much of the functionality of external pages had to be removed because of some tightening of privacy controls. Blur 3.0 will no longer support external pages (not that any developers adopted it anyways...)

These APIs will no longer be maintained. If you have a page that you would like to make, it is easier than ever. No more messing with this stuff, you just make a super simple Fragment that all developers know and love.

Blur is open source, that is where you should add any new pages that you would like to create.

What is this repository for?

Welcome to Blur, we are glad that you are taking an interest in our project and extending it to its potential!

This readme is going to explain our api and how everything works.

Before we get started, here are just a few links to help point you in the right direction:

Here are just a few helpful links to get you going:

1.) Hello World Example Page: https://github.com/klinker24/Blur-Hello-World-Page

2.) Hello World Example Card: https://github.com/klinker24/Blur-Hello-World-Card

3.) Advanced Examples (including Calculator Page, Blur Info Page, Weather Card, Next Alarm Card, and the Next Event Card): https://github.com/klinker24/Blur-Extension-Examples

I would start any project with the Hello World (card or page) as your base. They are easy to build off of.

Please note that this is kind of a long guide, but if you are serious about creating an extension (and I hope you are :) ), then it is absolutely necessary to understand how everything works in the background as well as the foreground. So I would encourage you to read carefully!


General Information

To start, we just wanted to explain a little bit about how the extentions work and what the app actually does in the background. The code that you create for your extention, whether that is a page, or a card, runs completely on Blur's context. We create an object from your class, and run the constructor from our API. That constructor then of course goes through, sets up your code, and runs everything that is necessary to display your extention. Again though, this isn't running in your apps context, it is running completely on ours, which can cause a few problems for things like shared preferences, accessing resources, accessing your cache, or any of your databases. We just wanted to make it clear that this isn't quite like DashClock which is able to broadcast updates from inside your own app. These extentions have to run within our app or they just do not work.

We will talk about a few of these limitations now, and how you can get around them if you so choose.

Resource Helper

The first part of our API that we will talk about is the ResourceHelper that we created. It is a pretty simple thing, but very powerful at the same time. It can be used to get resources (drawables, strings, dimens, booleans, etc) from any package installed on the device.

As I said, your Blur extension runs solely on our context, and the resources in Blur probably will never match the resources you are looking for. Our ResourceHelper will make it fairly simple though to get what you need, the only downside is that the system can't auto complete your resource names since they are sent in as strings.

Getting the resources from your app is straightforward though:

// first we need to create a ResourceHelper object
// this one will allow me to grab the resources from Talon
// just replace my package name with the package name for your app instead.
ResourceHelper resHelper = new ResourceHelper(getActivity(), "com.klinker.android.twitter");

// Now that we have the resHelper, it is a few simple calls to get the
// resources that you are looking for

// say we want a search drawable
Drawable d = resHelper.getDrawable("ic_action_search");

// the same type of methods can be used to get strings, animations, or other resources
String s = resHelper.getString("hello_world");

The ResourceHelper has to be used for finding views within your layouts as well.

// this will get the root layout of the page
View root = resHelper.getLayout("launcher_page_layout");

// now we want to find the hello world text view from within that layout
// we can't just do root.findViewById(R.id.hello_world_text_view") since we are running on
// a different context and that id will not exist
TextView textView1 = (TextView) root.findViewById(resHelper.getId("hello_world_text_view"));

The ResourceHelper will be your best friend when creating these things. If you aren't finding your resources correctly, it will be pretty obvious throughout your testing. Blank pages will appear, sometimes other pages wont work right, and sometimes you will get errors. Just depends on what you are doing with those views. It can be difficult to debug, but if something isn't working correctly, the ResourceHelper stuff is the first thing that I would try.

Shared Preference Management

SharedPreferences are a great way to save user options for your app, as I am sure you know. But they do present just a little challenge when trying to read your options from another app. Some folks use content providers instead to save preferences, but Jacob and I did not do that with Talon, EvolveSMS, or Blur, so we had to find ways to access those shared prefs from our launcher.

We went with the world readable preference route. We don't store any secure data in shared preferences, really only user options, so this was the way that we decided to go. I will step you through converting your prefs in a minute, but first, please think about the security of your app and what you do and do not want other applications to have access to. It isn't necessary to store all of your prefs as world readable if you do not want to, keep that in mind.

Before doing this, I tried using broadcasts to communicate between the apps, but this did not work at all for some reason. If you have a better method, feel free to share it.

So to convert ours, we saved the prefs to an external file, created a world readable SharedPreferences object, then wrote those saved prefs into the world readable object.

Here is my code, from Talon, to convert your shared preferences to be read from the launcher:

public static void updateToGlobalPrefs(final Context context) {
	// first alert the user, if you don't feel the need to do this
	// then run it in a background thread (since we are writing to files here)
	new AlertDialog.Builder(context)
		.setTitle("Settings Update")
		.setMessage("Talon has to update your settings preferences to prepare for some new things. This will override any old settings backups.")
		.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
		    @Override
		    public void onClick(DialogInterface dialogInterface, int i) {
			// excecute the global prefs async task
		        new WriteGlobalSharedPrefs(context).execute();
		    }
		})
		.create()
		.show();
}

static class WriteGlobalSharedPrefs extends AsyncTask<String, Void, Boolean> {

	ProgressDialog pDialog;
	Context context;

	public WriteGlobalSharedPrefs(Context context) {
	    this.context = context;
	}

	protected void onPreExecute() {
	    super.onPreExecute();
	    pDialog = new ProgressDialog(context);
	    pDialog.setMessage("Saving...");
	    pDialog.setIndeterminate(false);
	    pDialog.setCancelable(false);
	    pDialog.show();

	}

	protected Boolean doInBackground(String... args) {
	    // create a file in the external storage. You can delete this file after
	    // backup is done

	    File des = new File(Environment.getExternalStorageDirectory() + "/Talon/backup.prefs");
	    saveSharedPreferencesToFile(des, context);
	    loadSharedPreferencesFromFile(des, context);

	    return true;
	}

	protected void onPostExecute(Boolean deleted) {
	    try {
		pDialog.dismiss();
		Toast.makeText(context, "Save Complete", Toast.LENGTH_SHORT).show();
	    } catch (Exception e) {
		// not attached to activity
	    }

	    SharedPreferences sharedPrefs = context.getSharedPreferences("com.klinker.android.twitter_world_preferences",
		    Context.MODE_WORLD_READABLE + Context.MODE_WORLD_WRITEABLE);
	    
	    // now that the shared prefs are created as world readable (and writable in my case)
	    // feel free to delete that backup file as well as remove any prefs
 	    // that contain information you do not want readily available
	}
}

public static boolean loadSharedPreferencesFromFile(File src, Context context) {
	boolean res = false;
	ObjectInputStream input = null;

	try {
	    if (!src.getParentFile().exists()) {
		src.getParentFile().mkdirs();
		src.createNewFile();
	    }

	    input = new ObjectInputStream(new FileInputStream(src));
	    SharedPreferences.Editor prefEdit = context.getSharedPreferences("com.klinker.android.twitter_world_preferences",
		    Context.MODE_WORLD_READABLE + Context.MODE_WORLD_WRITEABLE)
		    .edit();

	    prefEdit.clear();

	    @SuppressWarnings("unchecked")
	    Map<String, ?> entries = (Map<String, ?>) input.readObject();

	    for (Map.Entry<String, ?> entry : entries.entrySet()) {
		Object v = entry.getValue();
		String key = entry.getKey();

		if (v instanceof Boolean) {
		    prefEdit.putBoolean(key, ((Boolean) v).booleanValue());
		} else if (v instanceof Float) {
		    prefEdit.putFloat(key, ((Float) v).floatValue());
		} else if (v instanceof Integer) {
		    prefEdit.putInt(key, ((Integer) v).intValue());
		} else if (v instanceof Long) {
		    prefEdit.putLong(key, ((Long) v).longValue());
		} else if (v instanceof String) {
		    prefEdit.putString(key, ((String) v));
		}
	    }

	    prefEdit.commit();

	    res = true;
	} catch (Exception e) {

	} finally {
	    try {
		if (input != null) {
		    input.close();
		}
	    } catch (Exception e) {

	    }
	}

	return res;
}

public static boolean saveSharedPreferencesToFile(File dst, Context context) {
	boolean res = false;
	ObjectOutputStream output = null;

	try {
	    if (!dst.getParentFile().exists()) {
		dst.getParentFile().mkdirs();
		dst.createNewFile();
	    }

	    output = new ObjectOutputStream(new FileOutputStream(dst));
	    SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);

	    // if they have converted to the world readable, then get those instead
	    if (!pref.getBoolean("version_2_2_7_1", true)) {
		pref = context.getSharedPreferences("com.klinker.android.twitter_world_preferences",
		        Context.MODE_WORLD_READABLE + Context.MODE_WORLD_WRITEABLE);
	    }

	    output.writeObject(pref.getAll());

	    res = true;
	} catch (Exception e) {

	} finally {
	    try {
		if (output != null) {
		    output.flush();
		    output.close();
		}
	    } catch (Exception e) {

	    }
	}

	return res;
}

Another part of the shared preferences is accessing them from the launcher. It is pretty straight forward. In your page or card, use this to get a instance of your applications shared prefs:

Context talonContext = context.createPackageContext("com.klinker.android.twitter", Context.CONTEXT_IGNORE_SECURITY);
SharedPreferences sharedPrefs = talonContext.getSharedPreferences("com.klinker.android.twitter_world_preferences",
                    Context.MODE_WORLD_READABLE + Context.MODE_WORLD_WRITEABLE);

There you go. I would like to caution you against using shared prefs to communicate between your app and the launcher though. For example, when creating talon's page, I ran into problems because I was storing the current timeline position as a shared preference. This did not work because you can only have one writable instance of shared prefs available at a time.

I ended up having to use a flag value in my content provider to mark the current tweet on the timeline. It works pretty well, but it is a little less efficient. So when designing your apps as pages, this is something to keep in mind, you may have to come up with creative ways to do things.

Remember that you can also start "exported" services or activities in your app from the launcher page. I found this as another effective method to control the flow of information.

For example, say I want the timeline to refresh when I enter the app, because of something that changed in the launcher page

First, declare an exported service in your AndroidManifest.xml:

<service android:name=".ui.launcher_page.HandleScrollService"
	android:exported="true" >
</service>

Then, simply start that service from the launcher page:

final Intent handleScroll = new Intent("android.intent.action.MAIN");
handleScroll.setComponent(new ComponentName("com.klinker.android.twitter", "com.klinker.android.twitter.ui.launcher_page.HandleScrollService"));
handleScroll.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
talonContext.startService(handleScroll);

You can use this service to write some kind of data into your app or whatever you need to make sure that timeline is refreshed when the user enters it.

Wow, that was a lot... While creating this project, shared preferences were definitely the most difficult part. Once you get this figured out and working, it should be a little smoother sailing!

Using Databases

Having quick access to your data is something that is essential to any Android developer now a days. One of the best ways to store user data is with SQL databases. They make everything pretty simple and any apps that handle are most likely going to encorporate them.

Because of the way we have to create the objects in our app and running your page within our own context. You aren't going to have access to the same databases you would within your own apps context.

Android has created an awesome way to share data between apps though, and that is content providers. By turning your SQL database into a content provider, you can use things like loaders to efficiently manage and handle your cursors through those databases. If you don't know much about loaders or content providers, it is something I would encourage you to look into. It can be a bit daunting at first, but something that is really amazing for your apps.

Content Providers are going to be necessary if you want your launcher page to be able to pull from the same data as your app. There are tons of guides online to turning your database into a content provider, just start with one and go with it. Don't forget that you will have to "export" that content provider in your AndroidManifest.xml as well, or else you won't be able to find it from your launcher extension. More about security with that on the next section as well.

Here is an example manifest item with the content provider exported though:

<provider
	android:name=".data.sq_lite.HomeContentProvider"
	android:multiprocess="true"
	android:exported="true"
	android:authorities="com.klinker.android.twitter.provider" />

Cache Management

Since, once again, your launcher page or card is running solely on Blur's context, that means that you do not have access to the same cache that you normally would in your app. Your app will only access and save to Blur's cache. This is just something to be aware of and something that your users may like to be aware of as well.

App Security

Another thing I want to talk about in this general section is security for your app. This is something we thought long and hard about while creating this project and something we couldn't take lightly. There is a fine line now a days about what is right for the user's security and functionality and an app like this, where opening up your data is necessary, can be tough and push that line.

There are a few things you can do to reduce the amount of data you are opening up to the android system for other apps to read. The first thing that I would recommend is Securing your Content Providers

I have found that the simplest and most effective way to do this is a UID check on the incoming query or write. This allows you to check to make sure the app that is performing it should have access to do so. It allows you to check the package name of the incoming app and either provide access to your content provider, or deny that access.

Here is how I check the UID of the incoming app in my Talon Content Provider:

private boolean checkUID(Context context) {
	int callingUid = Binder.getCallingUid();

	final PackageManager pm = context.getPackageManager();
	List<ApplicationInfo> packages = pm.getInstalledApplications(
		PackageManager.GET_META_DATA);

	int launcherUid = 0;
	int twitterUid = 0;

	// this is used to get the UID for both the launcher and talon
	// you can keep references to them for better efficiency 
	// as the UID is not something that changes

	for (ApplicationInfo packageInfo : packages) {
	    if(packageInfo.packageName.equals("com.klinker.android.twitter")){
		//get the UID for the selected app
		twitterUid = packageInfo.uid;
	    }
	    if(packageInfo.packageName.equals("com.klinker.android.launcher")){
		//get the UID for the selected app
		launcherUid = packageInfo.uid;
	    }
	}

	// just returns true if the calling id matches either Talon's UID or Blur's UID
	if (callingUid == launcherUid || callingUid == twitterUid) {
	    return true;
	} else {
	    return false;
	}
}

If the app doesn't match the UID of one of the apps you allow, you can throw a SecurityException or simply return a null cursor (on a query of the Content Provider) or stop the funtion before it inserts anything into your database.

I already talked about Shared Preferences and what you can do to secure those. Just make sure you aren't keeping any sensitve data in your world_readable prefs

The last part of the security discussion is Permissions.

Remember that since it is running within Blur, your page will NOT have the same permissions as your regular app! I repeat, WILL NOT have the same permissions as your regular app.

If Blur doesn't have a permission that you need, we cannot just go in and add it to our manifest for you, which is something you need to consider. You will have to come up with another way to get around using that permission on the launcher.

An example of this from EvolveSMS is sending texts. Obviously with KitKat came some different permissions with messaging, reading from/writing to the content provider, and your default messaging apps.

We needed to come up with a way to keep Evolve as the default, but still be able to send messages from the launcher page. Our way around this was to start a service (just like a showed above), put an extra on that intent with the message details, and send the SMS/MMS from that service. Remeber that it needs to be exported in the manifest again, to be able to access it.

This does leave one security problem though, anyone could go in and start that service from their apps. One way we thought of to get around this is to use the same type of UID check on those exported services that I showed for the content providers. UID isn't something that can be faked, so this would be considered a secure way to manage those exported services.

Security is something that you cannot take lightly in an app these days. Discussions about it are all over the media and we knew we would be fools not to mention it in an app like this.

I'm not saying it isn't a challenge, but it is up to the devs to make the security happen for any of their apps. It is very possible to be safe creating these extensions, just be careful what you do and don't assume that others have no malicious intents with your users data. I tend to be a trusting guy, but I am not the kind of guy that would leave my applications data open for anyone to view.

Just be careful and precise with security and you will have no problems with this.

Monetizing Your Extensions

One huge hurdle that we never would have imagined or predicted was the inability to read and instantiate objects of classes from paid packages.

This means that if you put your package on the play store for $.99, Blur will NOT be able to use it as an extention, no matter what you do. During testing, we tried long and hard to figure out why Talon wasn't working when I sent the launcher to my friends. I spent endless nights debugging and trying to figure out the reason for the errors in the logs they were sending me. They kept coming back and saying that the class couldn't be found on the DEX list.

Finally, I was able to get in contact with the developer of Dexplorer and he was able to help me understand the problem: you can't read classes from a paid package because of Google's security placed on them.

Bummer. This was a huge blow when we figured it out. But life goes on, we couldn't stop now, so I was able to parse the necessary data out of my talon package and place it into a seperate app that we could put up for free.

So this brings me to the main point: You cannot put your extensions up as paid packages. It will not work, no matter how hard you try.

If you plan on monetizing these things, you will have to rely on in-app purchases to do so. We aren't just saying this because we want extensions to be free, this is the truth, and if you try it, you will see exactly what I mean and experience the same frustrations that I did.

So that's a downer, but not the end of the world by any means.

Just a note: You may have seen the problems with Android Wear when it came out, in that it couldn't use paid packages. This is the same type of situation, in a paid package, because of Google's security on the Play Store, the only thing you can read from those packages are the resources. For the Android Wear problem, Google was able to provide a workaround by just changing the file structure, unfortunately that isn't at all how it works with Blur and this type of workaround is not an option.

Catch Those Errors

Something like this could be tough to debug, I know that. You don't have every device and neither do we. Something to keep in mind though, if something doesn't work on your side of things, it is going to crash the entire launcher. So please please please, DO YOUR ERROR trapping.

For debugging, at this point, you will have to rely on users getting you logs if something doesn't work. There is no way we can go through the play store reports for this thing and send them out to any dev that makes an extension. We have lives, and that would take up every second of them... So catch your errors, display a message to the user maybe, and get your data for them that way!

Unless someone else has a better solution for error handling, this is the way it is going to have to work.


Pages API

The best way to get going with these is just to go over the examples we have made then just experiment with your own!

How It Works

We wanted our pages to be something that people could pick up on right away, something that they have been using in their apps already, so it was pretty logically that they should be just generic fragments, and that is exactly what they are. The BaseLauncherPage that you need to extend on your pages is just an extention of a Fragment!

This means that you get all the same lifecycle stuff as a normal fragment (plus a few that we added in) and it should be fairly easy to understand how the layouts work and are set up.

Implementing a Launcher Page

The first thing that you are going to have to do is set up your manifest entry for the page.

Here is an example of Talon's Launcher Page AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.klinker.android.launcher.twitter_page"
          android:versionCode="1"
          android:versionName="1.0">

    <uses-sdk android:minSdkVersion="14"
        android:targetSdkVersion="19"/>

    <application
        android:allowBackup="true"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:theme="@android:style/Theme.Holo.Light.DarkActionBar"
        android:restoreAnyVersion="true">

        <meta-data android:name="launcher_fragment"
            android:value=".LauncherPage" />

    </application>

</manifest>

Looking at the meta-data element, you will see that the name is launcher_fragment (this is always what you should call it! That is how we read it into Blur), and the value points to the actual extention page. When putting in the value, you SHOULD NOT include the package name, only the path after the package.

Now, to create the actual page, I will just point you to a very simple example page that we made for a calculator: [https://github.com/klinker-apps/Blur-Extension-Examples/blob/master/src/com/klinker/android/launcher/calc_page/LauncherFragment.java](Calculator Page)

That page does a good job of showing the basics of creating your own and where to start. It provides examples of the resource helper and its actual implementation with finding and setting up the layout.

A few things to note:

  • I set up a padding on the bottom of it if the user has sdk 19 or above. This adds space for the nav bar at the bottom so the buttons don't go under it. You can check out how to find the sizes for it and setting it up in the code. This is something you should keep in mind though when making your own page.
  • You can set the get background method. This is used to choose what views to fade in and out. We have implemented ours a lot like the Google Now one. It will keep the normal elements and just fade the background behind them out.
  • We get the references to the context, res helper, and some general stuff in the onAttach() method. This is called when the fragment is created and attached to the screen.
  • A few things that you don't see in this example are the onFragmentsOpened() and onFragmentsClosed() methods. These are the extra lifecycle methods that we have added. Their names are pretty self explainitory, but they are called when the user swipes to open the pages or swipes to close out of the pages and return to the workspace.

To actually create the page, the API relies on one specific method that you HAVE to implement. It is really simple. Just return a new object of the class you are currently using:

    /**
     * Creates and instance of this fragment which is then returned to the pager adapter and displayed
     * @param position the position on the pager of this page
     * @return an instance of the LauncherFragment to be displayed
     */
    @Override
    public BaseLauncherPage getFragment(int position) {
        return new LauncherFragment();
    }

All in all, creating a page is almost just like making a fragment. With the exception of the res helper and shared prefs access, they are the same. Fragments are a powerful tool in Android now a days and I would encourage you to learn more about them to make your page the most funtional end efficient that it can be. Google has created some great docs on fragments here: http://developer.android.com/guide/components/fragments.html

If you would like more examples of pages, we will be making more of them and adding to our extra_pages examples soon. Right now the Blur Info Page is also up and open sourced in the same package as the calculator one that we linked to. The only thing to be aware of is that these examples won't have the manifest entries that are necessary for external packages to be picked up by our launcher.

Custom Classes

One thing that you are going to have to watch out for it the custom classes. When making something like this, Android's inflator won't be able to create and show a class that you have edited yourself. You can only use the basic views available in android (TextView, EditText, etc).

This doesn't mean in the slightest that it isn't possible to use things like the swipe refresh layout or some kind of custom text view. Instead of just adding them in the xml layout though, you have to add them dynamically when you are creating and setting up your views. For an example of this, check out the Blur Info Page and how it implements a custom SwipeRefreshLayout.

This is a more advanced topic, but it is something that is pretty important to create awesome elements in your page that will stun and amaze your users. If this kind of thing interest you, I would definitely encourage you to look into it. It isn't hard, just takes some time to understand what you are doing and how the peices all fit together.


Cards API

Obviously this isn't Google Now, and never will be. There is one company in the world with enough information and power to create a service like that, and they have already done so.

We wanted to make something a little bit different, that anyone could plug into and create their own cards for. A lot like dashclock, but with added funtionality, style, and developer's can do much more with it.

Again, the best way to get going with these is just to go over the examples we have made then just experiment a little bit!

How It Works

These pages are just an extention of FrameLayouts. That makes them easy to work with, easy to add elements to, and easy to extend upon. You basically just need to inflate your layout using the resource helper, then add it to the root view of the card.

The pages will get refreshed whenever the user pulls out the drawer (if it has been 15 mins since the last refresh), or whenever the user pulls to refresh.

It is important to note that these refreshes are done on a background thread that will give a callback to the ui when it is done. This allows for networked tasks as well as the UI thread not being stopped during an update. However, it also meanst that if you try to change the layout DURING the refresh, it will force close the launcher, so be very careful of that!

Implementing an Info Card

The first thing that you are going to have to do is set up your manifest entry for the card

You have to put the meta-data in the same spot on the manifest as talon's above example. For cards, there are 2 meta-data elements:

<meta-data android:name="launcher_card"
    android:value=".LauncherCardWeather" />

<meta-data android:name="launcher_card_title"
    android:value="Weather Card" />

The same type of convention goes into the launcher_card as the launcher_page element. The launcher_card_title is simply what you want displayed to the user when selecting it from the card picker.

Now, to create the actual card, I will just point you to the weather card we made: [https://github.com/klinker-apps/Blur-Extension-Examples/blob/master/src/com/klinker/android/launcher/info_page/cards/weather/WeatherCard.java](Weather Card)

That page does a good job of showing the basics of creating your own and where to start. This isn't the simplest of the example cards, but it is the most complete.

A few things to note:

  • This card uses a combination of settings types. Clicking the settings button on the card will first show a Google Now style settings (appearing right on the card), but it will also use a DialogFragment to allow the user to choose a location. Settings for the cards aren't to difficult, but you should choose a method that works well for you.
  • The settings for the card are saved in the launcher's shared prefs. So be sure to choose a very unique identifier for it!
  • There is a settingsClicked() method that you can override for when the user touches the settings button
  • You can specify at any time (except during a refresh) that you want to show or hide the settings button. Just call the showSettings(boolean) method with true to show the 3 dots in the top right, and false to hide them.
  • There is also an onCardPressed(View view) method that you can override. This is when the user actually clicks the individual card. With the weather, it just takes you to google's weather page

Something to consider with your cards is the possiblity of the user not actually needing it all the time. The weather card isn't a good example of this functionality because it is always there. To view an example of this, check out the next event card or the calendar card. It is a simple call to the shouldShow(boolean) method, where the boolean is just true if the card should be shown, or false otherwise. They will always default to true.

As with the launcher page, you need to implement the following method so that we can get an instance of your object! This example comes from the next alarm card:

/**
     * Used to initialize and create the card.
     * @param context
     * @return
     */
    public BaseCard getCard(Context context) {
        return new NextAlarmCard(context);
    }

So creating a card is just like creating a regular layout. The background methods are a bit different, but they aren't to complex and the examples go over fairly well what they do and when they are called.


Wrapping Up

Hopefully this got you excited about making some kind of extension for our launcher. There is so much that can be done here. We took Google's launcher one step up and made it so anyone could extend on it and make it their own.

Enjoy making these, once you get it down and understand what everything actually is, you can do absolutely anything with this.

It has been an awesome project to work on and something that I cannot wait to see how far it can go and what can actually be done when people get a hold of it with more time than us!

Cheers,

Luke and Jake Klinker


Building the library yourself

You can package the Blur APIs into a JAR for yourself if you'd like:

./gradlew clean jar

This will however not include the /res folder, so you'll be missing the card_action_overflow.png - your app may or may not work correctly like this on the launcher without this file.

You can package the Blur APIs into a recommended AAR for yourself with

./gradlew build

Outputs JAR will be in the /build/libs/ directory, output AAR will be in the /build/outputs/aar/ directory.

Gradle

To include the library in your gradle project:

compile 'com.klinkerapps:launcher-apis:+@aar'

Who do I talk to?

Something not clear or you have a better idea for things?

Email us at: luke@klinkerapps.com

We will do our best to get back to you!


License

Copyright 2014 Klinker Apps Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.