Search Function with Custom Search Suggestions

In this tutorial I will show you how you can create a dedicated search view with custom search suggestions („autocomplete“) in combination with a list of recent search queries. The data source for the autocomplete comes from a REST call that is executed every time the user types in a letter in the search field.

SearchView with custom search suggestions (autocomplete) loaded from ArrayList.

Search Functionality in Action

Have a look at this video to see it in action:

Prerequisites / General Info

In this project I am using the ButterKnife library for the binding of the layout and views (i.e. @BindView(R.id.relative_layout)). If you don’t use ButterKnife you have to alter the parts of the code snippets accordingly.

app // build.gradle

minSdkVersion 21
targetSdkVersion 26

implementation "com.jakewharton:butterknife:8.8.1"
implementation "com.zsoltsafrany:needle:1.0.0"

AndroidManifest.xml

Add this part to your AndroidManifest.xml file. Replace AppTheme.RedToolbar with whatever style you have for your toolbar in your project.

<activity
android:name=".SearchActivity"
android:launchMode="singleTop"
android:theme="@style/AppTheme.RedToolbar">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
<meta-data
android:name="android.app.default_searchable"
android:value=".SearchActivity" />

</activity>

res/xml/searchable.xml

Create a searchable.xml file and place it in the /res/xml subfolder in your project.

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="TYPE TO SEARCH"
android:label="LABEL"
android:searchSuggestSelection=" ?"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />

SearchActivity

SearchActivity.java is an activity with a toolbar with an active search input field that is initially empty until search results will populate the RecyclerList. While the user is typing search suggestions will be displayed in a menu below the search field. It also saves the last 5 submitted search queries in the SharedPreferences.

The class looks like this:

SearchActivity.java

public class SearchActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, OnSearchResultListItemSelectedListener, OnSearchCompletedListener {
private static String TAG = SearchActivity.class.getSimpleName();

static final int GRID_COLUMN_COUNT = 2;
private static final String COLUMN_ID = "_id";
private static final String COLUMN_SUGGESTION = "suggestion";
private static final String COLUMN_CONTEXT = "context";
private static final String COLUMN_SCORE = "score";
private static final String DEFAULT = "default";

private SearchResultListItemRecyclerViewAdapter searchResultListItemRecyclerViewAdapter;
private MovieSearchSuggestionAdapter movieSearchSuggestionAdapter;
private String queryString;

private SearchManager searchManager;
private SearchView searchView;

private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;

public static String[] columns = new String[]{COLUMN_ID, COLUMN_SUGGESTION, COLUMN_CONTEXT, COLUMN_SCORE};

@BindView(R.id.relative_layout)
RelativeLayout relativeLayout;
@BindView(R.id.searchresult_list)
RecyclerView recyclerView;
@BindView(R.id.empty)
View emptyView;
@BindView(R.id.empty_image_view)
ImageView emptyImageView;
@BindView(R.id.empty_label)
TextView emptyLabel;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
ButterKnife.bind(this);

getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);

searchResultListItemRecyclerViewAdapter = new SearchResultListItemRecyclerViewAdapter(new ArrayList<>(), this, this);

GridLayoutManager gridLayoutManager = new GridLayoutManager(this, GRID_COLUMN_COUNT);
recyclerView.setLayoutManager(gridLayoutManager);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
switch (searchResultListItemRecyclerViewAdapter.getItemViewType(position)) {
case ListItem.TYPE_HEADER:
// Header uses 2 columns in 2 column grid
return GRID_COLUMN_COUNT;
default:
// Movie uses 1 column in 2 column grid
return 1;
}
}
});

recyclerView.setAdapter(searchResultListItemRecyclerViewAdapter);
recyclerView.addItemDecoration(new MovieGridListItemSpacingDecoration(GRID_COLUMN_COUNT, getResources().getDimensionPixelSize(R.dimen.grid_list_item_spacing), true));

movieSearchSuggestionAdapter = new MovieSearchSuggestionAdapter(getApplicationContext(), null, null, searchView);

sharedPreferences = getSharedPreferences(Preferences.CINEMAN, Context.MODE_PRIVATE);
editor = sharedPreferences.edit();
}

@Override
protected void onNewIntent(Intent intent) {
handleIntent(intent);
}

@Override
public void onStart() {
super.onStart();
if (searchResultListItemRecyclerViewAdapter.getItemCount() == 0) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyLabel.setText("Universelle Filmsuche");
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}

}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_view_options_menu, menu);

searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchView = (SearchView) menu.findItem(R.id.search).getActionView();
searchView.setMaxWidth(Integer.MAX_VALUE);
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setOnQueryTextListener(this);
searchView.setIconifiedByDefault(true);
searchView.setQueryHint(getString(R.string.movies_search));
searchView.setElevation(10);
searchView.onActionViewExpanded(); // Activate on launch
searchView.setSuggestionsAdapter(movieSearchSuggestionAdapter);

return true;
}

@OnClick(R.id.empty)
void clickEmptyView() {
searchView.onActionViewExpanded();
}

@Override
public boolean onQueryTextSubmit(String query) {
queryString = query.trim();
if (queryString.length() > 2) {
handleAndUpdateSearchHistoryList(queryString);
searchResultListItemRecyclerViewAdapter.globalMovieSearch(query);
} else {
Handler handler = new Handler();
handler.postDelayed(() -> Snackbar.make(relativeLayout, "Bitte gebe mindestens drei Zeichen für die Suche ein.", Snackbar.LENGTH_LONG).show(), 400);
}

ViewUtils.hideKeyboard(this, searchView);
return true;
}

@Override
public boolean onQueryTextChange(String newText) {
queryString = newText.trim();
autocomplete(queryString);
return true;
}

private void autocomplete(final String query) {
final MatrixCursor matrixCursor = new MatrixCursor(columns);
final Object[] object = new Object[]{0, DEFAULT, DEFAULT, DEFAULT};

// Get the Search History Items
String searchHistoryCommaSeparated = sharedPreferences.getString(Preferences.PREFS_SEARCH_HISTORY_ITEMS, "");
List<String> searchHistoryItemList = new ArrayList<>();
String[] searchHistoryArray = searchHistoryCommaSeparated.split(",");
if (searchHistoryArray.length > 1) {
Collections.addAll(searchHistoryItemList, searchHistoryArray);
}

// Delete duplicate search history items
HashSet<String> hashSet = new HashSet<>();
hashSet.addAll(searchHistoryItemList);
searchHistoryItemList.clear();
searchHistoryItemList.addAll(hashSet);

if ((query != null) && (query.trim().length() > 2)) {
CinemanAPI.movie().autocomplete(query.trim(), new RepositoryEntityListCallback<Autocomplete>() {

@Override
public void onSuccess(List<Autocomplete> tList, boolean isCachedResponse) {

int i = 0;
while (i < tList.size()) {
object[0] = i;
object[1] = tList.get(i).getSuggestion();
object[2] = tList.get(i).getContext();
object[3] = tList.get(i).getScore();

matrixCursor.addRow(object);
i++;
}

for (String historyQuery : searchHistoryItemList) {
Autocomplete ac = new Autocomplete();
ac.setContext("history");
ac.setScore(1000);
ac.setSuggestion(historyQuery);
tList.add(ac);

object[0] = i;
object[1] = ac.getSuggestion();
object[2] = ac.getContext();
object[3] = ac.getScore();

matrixCursor.addRow(object);
i++;
}

updateAdapter(matrixCursor, tList);
}

@Override
public void onFailure(Exception e) {
Log.w(TAG, e.getLocalizedMessage());
}
});
} else if ((query != null) && (query.trim().length() > 0)) {
List<Autocomplete> tList = new ArrayList<>();

int i = 0;
for (String historyQuery : searchHistoryItemList) {
if (historyQuery.length() > 0) {
Autocomplete ac = new Autocomplete();
ac.setContext("history");
ac.setScore(1000);
ac.setSuggestion(historyQuery);
tList.add(ac);

object[0] = i;
object[1] = ac.getSuggestion();
object[2] = ac.getContext();
object[3] = ac.getScore();

matrixCursor.addRow(object);
i++;
}
}

updateAdapter(matrixCursor, tList);
} else {
updateAdapter(matrixCursor, new ArrayList<>());
}
}

private void updateAdapter(MatrixCursor matrixCursor, List<Autocomplete> tList) {
movieSearchSuggestionAdapter = new MovieSearchSuggestionAdapter(getApplicationContext(), matrixCursor, tList, searchView);
searchView.setSuggestionsAdapter(movieSearchSuggestionAdapter);
movieSearchSuggestionAdapter.swapCursor(matrixCursor);
movieSearchSuggestionAdapter.notifyDataSetChanged();
}

private void updateUI() {
if (searchResultListItemRecyclerViewAdapter.getItemCount() == 0) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyImageView.setImageResource(R.drawable.shrug);
emptyLabel.setText("NOTHING FOUND");
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
}

@Override
public void onTheatreListItemSelected(Theatre theatre) {
// TODO kommt später mal...
}

@Override
public void onMovieGridListItemSelected(Movie movie) {
if (movie != null) {
// Intent intent = new Intent(this, MovieDetailActivity.class);
// intent.putExtra(movie);
// startActivity(intent);
}
}

private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
queryString = intent.getStringExtra(SearchManager.QUERY);
if (queryString.length() > 1) {
searchResultListItemRecyclerViewAdapter.globalMovieSearch(queryString);
searchView.clearFocus();
}
}
}

private void handleAndUpdateSearchHistoryList(String nextSearchQueryAddition) {
String searchHistoryCommaSeparated = sharedPreferences.getString(Preferences.PREFS_SEARCH_HISTORY_ITEMS, "");
List<String> searchHistoryItemList = new ArrayList<>();

String[] searchHistoryArray = searchHistoryCommaSeparated.split(",");
if (searchHistoryArray.length > 0) {
Collections.addAll(searchHistoryItemList, searchHistoryArray);
}

searchHistoryItemList.add(nextSearchQueryAddition);
if (searchHistoryItemList.size() > 5) {
searchHistoryItemList = searchHistoryItemList.subList(1, searchHistoryItemList.size()); // Limit to 5 items
}

StringBuilder csvBuilder = new StringBuilder();
for (String query : searchHistoryItemList) {
csvBuilder.append(query);
csvBuilder.append(",");
}

String historyQueriesString = csvBuilder.toString();
String historyQueriesCommaSeparated = historyQueriesString.substring(0, historyQueriesString.length() - ",".length());

editor.putString(Preferences.PREFS_SEARCH_HISTORY_ITEMS, historyQueriesCommaSeparated);
editor.apply();
}

@Override
public void onOnSearchCompleted() {
updateUI();

// Hide the autocomplete dropdown list
onQueryTextChange("");
}

@Override
public boolean onSuggestionSelect(int position) {
searchView.clearFocus();
return false;
}

@Override
public boolean onSuggestionClick(int position) {
searchView.clearFocus();
return false;
}
}

OnSearchResultListItemSelectedListener.java

interface OnSearchResultListItemSelectedListener {
void onMovieGridListItemSelected(Movie movie);
}

RepositoryEntityListCallback.java

public abstract class RepositoryEntityListCallback<T> {
public abstract void onSuccess(List<T> entityList, boolean isCachedResponse);

public abstract void onFailure(Exception e);
}

Autocomplete.java

public class Autocomplete {
private int score;
private String suggestion;
private String context;

public int getScore() {
return score;
}

public void setScore(int score) {
this.score = score;
}

public String getSuggestion() {
return suggestion;
}

public void setSuggestion(String suggestion) {
this.suggestion = suggestion;
}

public String getContext() {
return context;
}

public void setContext(String context) {
this.context = context;
}

}

activity_search.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/relative_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
tools:context=".SearchActivity">

<android.support.v7.widget.RecyclerView xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/searchresult_list"
android:name=".SearchActivity"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="GridLayoutManager"
tools:context=".SearchActivity"
tools:listitem="@layout/movie_grid_list_item" />

<RelativeLayout
android:id="@+id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
android:clickable="true"
android:visibility="gone">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"
android:paddingBottom="33dp">

<ImageView
android:id="@+id/empty_image_view"
app:srcCompat="@drawable/ic_onboarding_map"
android:layout_width="match_parent"
android:layout_height="67dp"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/empty_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingLeft="48dp"
android:paddingTop="16dp"
android:paddingRight="48dp"
android:text="@string/settings_regions_empty_alert"
android:textAppearance="@style/TextAppearance.primaryText" />

</LinearLayout>

</RelativeLayout>
</RelativeLayout>

search_view_options_menu.xml

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

<item
android:id="@+id/search"
android:icon="@drawable/ic_search"
android:showAsAction="always"
android:title="@string/movies_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always"
tools:ignore="AppCompatResource" />

</menu>

movie_grid_list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<ImageView
android:id="@+id/moviePoster"
android:layout_width="match_parent"
android:layout_height="@dimen/grid_list_item_poster_height"
android:scaleType="centerCrop" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorGridItemBackground"
android:orientation="horizontal">

<TextView
android:id="@+id/movieNameLabel"
android:gravity="center_vertical|left"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_marginTop="6dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:paddingLeft="@dimen/grid_list_item_label_padding_left_right"
android:textAppearance="@style/TextAppearance.posterText" />

<LinearLayout
android:id="@+id/watchlistButton"
android:layout_width="48dp"
android:layout_height="56dp"
android:clickable="true">

<ImageView
android:id="@+id/watchlistButtonImageView"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="8dp"
android:layout_marginTop="14dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_add_circle_outline_black" />

</LinearLayout>

</LinearLayout>

</LinearLayout>

MovieSearchSuggestionAdapter.java

public class MovieSearchSuggestionAdapter extends CursorAdapter implements View.OnClickListener {
private final static String TAG = MovieSearchSuggestionAdapter.class.getSimpleName();

private Context context;
private final LayoutInflater mInflater;
private ImageView imageView;
private TextView suggestionTextView;
private SearchView searchView;

private final List<Autocomplete> autocompleteList;

public MovieSearchSuggestionAdapter(final Context context, final Cursor cursor, final List<Autocomplete> autocompleteList, SearchView searchView) {
super(context, cursor, 0);

this.context = context;
this.searchView = searchView;
this.autocompleteList = autocompleteList;
this.mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
final View view = mInflater.inflate(R.layout.search_result_item_layout, parent, false);

imageView = view.findViewById(R.id.icon);
suggestionTextView = view.findViewById(R.id.suggestion);

return view;
}

@Override
public void bindView(View view, Context context, Cursor cursor) {
final int position = cursor.getPosition();
if (cursorInBounds(position)) {

final Autocomplete auto = autocompleteList.get(position);
suggestionTextView.setText(auto.getSuggestion());

if (auto.getContext().equalsIgnoreCase("movie")) {
imageView.setImageResource(R.drawable.ic_search);
} else if (auto.getContext().equalsIgnoreCase("history")) {
imageView.setImageResource(R.drawable.ic_time);
}

view.setTag(position);
view.setOnClickListener(this);
} else {
// Something went wrong
}
}

private boolean cursorInBounds(final int position) {
return position < autocompleteList.size();
}

@Override
public void onClick(final View view) {
final int position = (Integer) view.getTag();

if (cursorInBounds(position)) {
final Autocomplete selected = autocompleteList.get(position);

ViewUtils.hideKeyboard(context, searchView);
searchView.setQuery(selected.getSuggestion(), true);
} else {
// Something went wrong
}
}


}

sarch_result.item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorBackground">

<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_margin="10dp"
android:padding="5dp"
android:src="@drawable/ic_search" />

<TextView
android:id="@+id/suggestion"
android:layout_width="wrap_content"
android:layout_height="23dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/icon" />

</RelativeLayout>

SearchResultListItemRecyclerViewAdapter.java

class SearchResultListItemRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final static String TAG = SearchResultListItemRecyclerViewAdapter.class.getSimpleName();

private List<Movie> movieList;
private List<ListItem> listItems;
private final OnSearchCompletedListener onSearchCompletedListener;
private final OnSearchResultListItemSelectedListener onSearchResultListItemSelectedListener;

SearchResultListItemRecyclerViewAdapter(List<Movie> items, OnSearchResultListItemSelectedListener onSearchResultListItemSelectedListener, OnSearchCompletedListener onSearchCompletedListener) {
listItems = new ArrayList<>();
this.onSearchResultListItemSelectedListener = onSearchResultListItemSelectedListener;
this.onSearchCompletedListener = onSearchCompletedListener;
setMovieList(items);

globalMovieSearch(null);
}

@Override
public int getItemViewType(int position) {
return listItems.get(position).getType();
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ListItem.TYPE_HEADER) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.generic_list_header_item, parent, false);
return new GenericListHeaderViewHolder(itemView);
}

View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.movie_search_result_grid_list_item, parent, false);
return new MovieSearchResultGridListItemViewHolder(itemView);
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int type = getItemViewType(position);

if (type == ListItem.TYPE_HEADER) {
GenericListHeaderItem genericListHeaderItem = (GenericListHeaderItem) listItems.get(position);
GenericListHeaderViewHolder genericListHeaderItemViewHolder = (GenericListHeaderViewHolder) holder;
genericListHeaderItemViewHolder.setLabelText(genericListHeaderItem.getLabel());
return;
} else {
final MovieGridListItem movieGridListItem = (MovieGridListItem) listItems.get(position);
MovieSearchResultGridListItemViewHolder movieSearchResultGridListItemViewHolder = (MovieSearchResultGridListItemViewHolder) holder;
movieSearchResultGridListItemViewHolder.setMoviePosition(movieGridListItem.getMoviePosition());
movieSearchResultGridListItemViewHolder.setMovie(movieGridListItem.getMovie(), CinemanApplication.getCurrentAppInstance().getApplicationContext());
movieSearchResultGridListItemViewHolder.view.setOnClickListener(v -> onSearchResultListItemSelectedListener.onMovieGridListItemSelected(movieGridListItem.getMovie()));
}
}


@Override
public int getItemCount() {
return listItems.size();
}

void setMovieList(List<Movie> movieList) {
this.movieList = movieList;
}

/**
* Replace "
CinemanAPI.movie().searchMovies()" with your own REST call / data access with the search query from the parameter for the search. The return type in this example is an ArrayList with Movie objects which gets forwarded to the formatMovieList() method where we format the list to head- and body-type of recycler list (or grid for that matter) items. This way we could group the search results in various sections i.e. movies, theatres etc.
*
*
@param query
*/
public void globalMovieSearch(final String query) {
if ((query != null) && (query.trim().length() > 2)) {
CinemanAPI.movie().searchMovies(query.trim(), new RepositoryEntityListCallback<Movie>() {

@Override
public void onSuccess(List<Movie> mList, boolean isCachedResponse) {
movieList.clear();
movieList.addAll(mList);

formatMovieList(mList);
}

@Override
public void onFailure(Exception e) {
Log.e(TAG, e.getLocalizedMessage());
formatMovieList(new ArrayList<>());
}
});
} else {
movieList.clear();
}
}

/**
* Loop through the movie list and create head- and movie items
*
*
@param mList
*/
private void formatMovieList(List<Movie> mList) {
final List<ListItem> newListItems = new ArrayList<>();

Needle.onBackgroundThread().withTaskType("globalMovieSearch").serially().execute(() -> {
GenericListHeaderItem genericListHeaderItem = new GenericListHeaderItem();
genericListHeaderItem.setLabel(CinemanApplication.getCurrentAppInstance().getString(R.string.movies_title));
newListItems.add(genericListHeaderItem);

int i = 2;
for (Movie movie : mList) {
MovieGridListItem movieGridListItem = new MovieGridListItem();
movieGridListItem.setMoviePosition(++i);
movieGridListItem.setMovie(movie);
newListItems.add(movieGridListItem);
}

if (mList.size() == 0) {
newListItems.clear();
}

Needle.onMainThread().execute(() -> {
listItems.clear();
listItems.addAll(newListItems);
notifyDataSetChanged();
onSearchCompletedListener.onOnSearchCompleted();
});
});
}

}

generic_list_header_item.xml

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

<TextView
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.subHeader" />
</LinearLayout>

movie_search_result_grid_list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/ticketingHighlightColor">

<ImageView
android:id="@+id/movie_poster"
android:layout_width="match_parent"
android:layout_height="@dimen/grid_list_item_poster_height"
android:scaleType="centerCrop" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/movie_poster"
android:layout_alignParentBottom="true"
android:background="@color/colorGridItemBackground"
android:orientation="horizontal">

<TextView
android:id="@+id/movie_title_label"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginLeft="@dimen/grid_list_item_label_padding_left_right"
android:layout_marginTop="6dp"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical|left"
android:maxLines="2"
android:text="Movie Title"
android:textAppearance="@style/TextAppearance.posterText" />

</LinearLayout>

</RelativeLayout>

GenericListHeaderViewHolder.java

class GenericListHeaderViewHolder extends RecyclerView.ViewHolder {
final View view;
private String labelText;

@BindView(R.id.label)
TextView labelView;

String getLabelText() {
return labelText;
}

void setLabelText(String labelText) {
this.labelText = labelText;
labelView.setText(labelText);
}

GenericListHeaderViewHolder(View view) {
super(view);
this.view = view;
ButterKnife.bind(this, view);
}

@Override
public String toString() {
return super.toString() + " '" + labelView.getText() + "'";
}
}

MovieSearchResultGridListItemViewHolder.java

class MovieSearchResultGridListItemViewHolder extends RecyclerView.ViewHolder {
private final static double posterRatio = 1.3825;
final View view;
private Movie movie;
private int moviePosition = -1;
private ColorDrawable placeHolderColorDrawable;

@BindView(R.id.movie_poster)
ImageView moviePoster;
@BindView(R.id.movie_title_label)
TextView movieNameLabel;


Movie getMovie() {
return movie;
}

void setMovie(Movie movie, Context context) {
this.movie = movie;
movieNameLabel.setText(movie.getTitle());
if (movie.getColorMeta() != null && movie.getColorMeta().getPosterBest() != null && movie.getColorMeta().getPosterBest().size() > 0) {
String hexColor = String.format("#%06X", (0xFFFFFF & movie.getColorMeta().getPosterBest().get(0)));
placeHolderColorDrawable.setColor(Color.parseColor(hexColor));
Picasso.with(context).load(movie.getPoster().getUrl2x()).placeholder(placeHolderColorDrawable).into(moviePoster);
} else {
Picasso.with(context).load(movie.getPoster().getUrl2x()).into(moviePoster);
}

public int getMoviePosition() {
return moviePosition;
}

public void setMoviePosition(int moviePosition) {
this.moviePosition = moviePosition;
}

MovieSearchResultGridListItemViewHolder(View view) {
super(view);
this.view = view;
ButterKnife.bind(this, view);

placeHolderColorDrawable = new ColorDrawable();
int imageWidth = (CinemanApplication.getCurrentAppInstance().getScreenWidth() - ((int) view.getResources().getDimension(R.dimen.grid_list_item_spacing) * (MovieGridListFragment.GRID_COLUMN_COUNT + 1))) / MovieGridListFragment.GRID_COLUMN_COUNT;
int desiredHeight = (int) (imageWidth * posterRatio);
if (moviePoster.getLayoutParams().height != desiredHeight) {
moviePoster.getLayoutParams().height = desiredHeight;
moviePoster.requestLayout();
}
}

@Override
public String toString() {
return super.toString() + " '" + movieNameLabel.getText() + "'";
}
}

Launching the Search

To start the search add this code block to any other activity or button click event.

Intent intent = new Intent(getActivity(), SearchActivity.class);
startActivity(intent);

Got improvements or questions? Or is anything missing? Let me know in the comments section below.

Open external Android app from within Android app

You can open any external Android application from within your Android application by pressing a button (or any other event). All you need to know is the namespace of the application you want to launch.

For this functionality use following code snippet:

public class MainActivity extends Activity {</p>
<p>private Button openAppButton;</p>
<p>@Override<br />
protected void onCreate(Bundle savedInstanceState) {<br />
super.onCreate(savedInstanceState);<br />
setContentView(R.layout.activity_main);</p>
<p>openAppButton = (Button) findViewById(R.id.openApp);<br />
openAppButton.setOnClickListener(new OnClickListener() {</p>
<p>@Override<br />
public void onClick(View arg0) {</p>
<p>Intent i = new Intent();<br />
PackageManager manager = getPackageManager();<br />
i = manager.getLaunchIntentForPackage("launching.app.namespace");<br />
i.addCategory(Intent.CATEGORY_LAUNCHER);<br />
startActivity(i);<br />
}</p>
<p>});<br />
}</p>
<p>}<br />

In this code replace „launching.app.namespace“ with the namespace of the app you want to launch. If you don’t know the namespace of the app you can go to Google Play and search for the app. Inside the URL the namespace is mentioned as the „id“. For example, Google Maps has following URL:

 https://play.google.com/store/apps/details?id=com.google.android.apps.maps

As you can see, the value of id is „com.google.android.apps.maps“ – this is the app’s namespace.

 

Wrapping a MGWT Webapp with PhoneGap into a Native Android App

Here is an example how to make a simple MGWT Webapp and wrap it with PhoneGap into a native Android application. With MGWT to you can create fantastic mobile web applications with the look and feel of native mobile Android or iOS apps.

  • Install Eclipse IDE
  • Install GWT plugin into Eclipse
  • Download the MGWT jar file
  • Download the PhoneGap (Cordova) jar file
  • Create a new GWT project („HelloWorld“) with Eclipse (the official „Getting started“ documentation for GWT can be found here).
  • Add following jar files into the /libs folder of the newly created GWT project:
    mgwt.jar
    gwtphonegap.jar
  • Add these lines of code into the HelloWorld.gwt.xml file:
    <br />
    <!-- Other module inherits --><br />
    <inherits name="com.googlecode.mgwt.MGWT"/><br />
    <inherits name='com.googlecode.gwtphonegap.PhoneGap' /><set-property name="user.agent" value="safari" /><br />
    <set-configuration-property name="mgwt.css" value="pretty" /><br />
    
  • Develop your GWT app as you wish using the MGWT library (the MGWT „Getting started“ documentation to be found here). Here is a simple and easy example:
    <br />
    public class HelloWorld implements EntryPoint {public void onModuleLoad() {<br />
    // set viewport and other settings for mobile<br />
    MGWT.applySettings(MGWTSettings.getAppSetting());// build animation helper and attach it<br />
    AnimationHelper animationHelper = new AnimationHelper();<br />
    RootPanel.get().add(animationHelper);// build some UI<br />
    LayoutPanel layoutPanel = new LayoutPanel();<br />
    Button button = new Button("Hello World! Woohoo!");<br />
    layoutPanel.add(button);// animate<br />
    animationHelper.goTo(layoutPanel, Animation.SLIDE);}}<br />
    
  • Create new native Android project with Eclipse. With this we are going to wrap the MGWT project into a native app.
  • Add the PhoneGap (or Cordova) jar file into the newly created Android project’s /libs folder.
  • Create subfolders /assets/www in that project. Make sure to add the PhoneGap.js (JavaScript) file into the /assets/www folder!
  • Create a simple index.html file in the /assets/www folder. Here a simple example of the html file:
    <br />
    <head><br />
    <title>Cordova</title><br />
    <script type="text/javascript" charset="utf-8" src="cordova-2.3.0.js"></script><br />
    </head><br />
    <body><br />
    <h1>Hello World</h1><br />
    <h2>PhoneGap works!</h2><br />
    </body><br />
    </html><br />
    
  • Add following permissions to the Android project’s manifest file:
    <br />
    <uses-permission android:name="android.permission.CAMERA" /><br />
    <uses-permission android:name="android.permission.VIBRATE" /><br />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><br />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><br />
    <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" /><br />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" /><br />
    <uses-permission android:name="android.permission.INTERNET" /><br />
    <uses-permission android:name="android.permission.RECEIVE_SMS" /><br />
    <uses-permission android:name="android.permission.RECORD_AUDIO" /><br />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /><br />
    <uses-permission android:name="android.permission.READ_CONTACTS" /><br />
    <uses-permission android:name="android.permission.WRITE_CONTACTS" /><br />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><br />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><br />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" /><br />
    <uses-permission android:name="android.permission.BROADCAST_STICKY" /><br />
    
  • Change the MainActivity class of the Android project that it looks like this:
    <br />
    public class MainActivity extends DroidGap {@Override<br />
    public void onCreate(Bundle savedInstanceState) {<br />
    super.onCreate(savedInstanceState);<br />
    // setContentView(R.layout.activity_main);<br />
    super.loadUrl("file:///android_asset/www/index.html");<br />
    }@Override<br />
    public boolean onCreateOptionsMenu(Menu menu) {<br />
    // Inflate the menu; this adds items to the action bar if it is present.<br />
    getMenuInflater().inflate(R.menu.activity_main, menu);<br />
    return true;<br />
    }}<br />
    
  • Build the Android project and see if the index.html file is being displayed after launching the app.
  • Compile (Google ->GWT Compile) the MGWT project. The compiled code can be found inside of the /war folder of the MGWT project.
  • Copy all the content of the /war folder of the MGWT project into the /assets/www folder in the Android project.
  • In the Android project in the MainActivity class change this line of code:
    <br />
    super.loadUrl("file:///android_asset/www/index.html");<br />
    

    to this

    <br />
    super.loadUrl("file:///android_asset/www/HelloWorld.html");<br />
    

    (or however the main html file of the MGWT project is named)

  • Run the Android project again. Now you should see the content of the MGWT app displayed acting like a native Android application. If that looks okay you can export the project to an APK file! Done!

Android SDK: Add a repeating AlarmManager to the autostart (displaying messages in the Notification Bar)

Add a repeating AlarmManager to the Android autostart (displaying messages in the Notification Bar)

This is a tutorial on how you can set an AlarmManager to continue doing its task even after the user has rebooted his phone. In this example the AlarmManager will repeatedly display a message in the notification bar of your Android phone after you rebooted it.

  • Create a new Android application in Eclipse called „OnBootCompletedAlarmManagerExample“. The namespace is on.boot.completed.
  • The content of the main Activity looks like this:

    OnBootCompletedExampleActivity.java

    <br />
    import android.app.Activity;<br />
    import android.app.NotificationManager;<br />
    import android.content.Context;<br />
    import android.os.Bundle;</p>
    <p>public class OnBootCompletedExampleActivity extends Activity {</p>
    <p>/** Called when the activity is first created. */<br />
    @Override<br />
    public void onCreate(Bundle savedInstanceState) {<br />
    super.onCreate(savedInstanceState);<br />
    setContentView(R.layout.main);<br />
    }</p>
    <p>@Override<br />
    protected void onResume() {<br />
    super.onResume();</p>
    <p>// Clear the Notification Bar after you've clicked on the message in the Notification Bar<br />
    NotificationManager nMgr = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);<br />
    nMgr.cancelAll();<br />
    }<br />
    }<br />
    
  • Create a new class called Autostart.java which extends BroadcastReceiver:

    Autostart.java

    <br />
    import android.content.BroadcastReceiver;<br />
    import android.content.Context;<br />
    import android.content.Intent;<br />
    import android.util.Log;</p>
    <p>public class Autostart extends BroadcastReceiver {</p>
    <p>/**<br />
    * Listens for Android's BOOT_COMPLETED broadcast and then executes<br />
    * the onReceive() method.<br />
    */<br />
    @Override<br />
    public void onReceive(Context context, Intent arg1) {<br />
    Log.d("Autostart", "BOOT_COMPLETED broadcast received. Executing starter service.");</p>
    <p>Intent intent = new Intent(context, StarterService.class);<br />
    context.startService(intent);<br />
    }<br />
    }<br />
    

    In the AndroidManifest.xml file we are going to add this class as a receiver. See the details for that further below. This class will listen to the broadcast call the Android OS sends after the boot sequence has finished (meaning after the phone started up).

  • Create another class called StarterService.java:

    StarterService.java

    <br />
    import android.app.AlarmManager;<br />
    import android.app.PendingIntent;<br />
    import android.app.Service;<br />
    import android.content.Context;<br />
    import android.content.Intent;<br />
    import android.os.IBinder;<br />
    import android.util.Log;<br />
    import android.widget.Toast;</p>
    <p>public class StarterService extends Service {<br />
    private static final String TAG = "MyService";</p>
    <p>/**<br />
    * The started service starts the AlarmManager.<br />
    */<br />
    @Override<br />
    public void onStart(Intent intent, int startid) {<br />
    Intent i = new Intent(this, NotificationBarAlarm.class);<br />
    i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);</p>
    <p>PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);</p>
    <p>// Repeat the notification every 15 seconds (15000)<br />
    AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);<br />
    am.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 15000, pi);</p>
    <p>Toast.makeText(this, "My Service started", Toast.LENGTH_LONG).show();<br />
    Log.d(TAG, "onStart");<br />
    }</p>
    <p>@Override<br />
    public IBinder onBind(Intent intent) {<br />
    return null;<br />
    }</p>
    <p>@Override<br />
    public void onDestroy() {<br />
    Toast.makeText(this, "My Service stopped", Toast.LENGTH_LONG).show();<br />
    Log.d(TAG, "onDestroy");<br />
    }<br />
    }<br />
    

    When the class Autostart receives the BOOT_COMPLETED broadcast from Android OS it will start the StarterService which then starts the AlarmManager which holds the PendingIntent object „pi“ which contains the NotificationBarAlarm class. NotificationBarAlarm displays a message in the Notification Bar of the Android device.

  • Next create a class called NotificationBarAlarm.java. As mentioned above this class is sending a message to the phone’s Notification Bar. The AlarmManager we called in the above class will call NotificationBarAlarm in an interval of 15 seconds after the phone has been rebooted.

    NotificationBarAlarm.java

    <br />
    import android.app.Notification;<br />
    import android.app.NotificationManager;<br />
    import android.app.PendingIntent;<br />
    import android.content.BroadcastReceiver;<br />
    import android.content.Context;<br />
    import android.content.Intent;<br />
    import android.net.Uri;<br />
    import android.util.Log;</p>
    <p>public class NotificationBarAlarm extends BroadcastReceiver {</p>
    <p>NotificationManager notifyManager;</p>
    <p>@Override<br />
    public void onReceive(Context context, Intent intent) {</p>
    <p>Log.d("NotificationAlarm", "onReceive");</p>
    <p>notifyManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);</p>
    <p>// This Activity will be started when the user clicks the notification<br />
    // in the notification bar<br />
    Intent notificationIntent = new Intent(context, OnBootCompletedExampleActivity.class);</p>
    <p>PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);<br />
    Notification notif = new Notification(R.drawable.ic_stat_rooster_col, "A new notification just popped in!", System.currentTimeMillis());</p>
    <p>// Play sound?<br />
    // If you want you can play a sound when the notification shows up.<br />
    // Place the MP3 file into the /raw folder.<br />
    notif.sound = Uri.parse("android.resource://" + context.getPackageName() + "/" + R.raw.jingle);</p>
    <p>notif.setLatestEventInfo(context, "Notification Title", "Notification Text", contentIntent);</p>
    <p>notifyManager.notify(1, notif);<br />
    }<br />
    }<br />
    
  • And finally we update the AndroidManifest.xml with the changes we need to do in order for the application to work how we want:

    AndroidManifest.xml

    <br />
    <?xml version="1.0" encoding="utf-8"?><br />
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="on.boot.completed"
        android:installLocation="internalOnly"
        android:versionCode="12"
        android:versionName="1.1.8" ></p>
    <p>    <uses-sdk
            android:minSdkVersion="7"
            android:targetSdkVersion="9" /></p>
    <p>    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><br />
        <uses-permission android:name="android.permission.READ_PHONE_STATE" /><br />
        <uses-permission android:name="android.permission.WAKE_LOCK" /><br />
        <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /></p>
    <p>    <application
            android:icon="@drawable/icon_w_bg"
            android:label="@string/app_name" ><br />
            <activity
                android:name="on.boot.completed.OneFunFactADayActivity"
                android:label="@string/app_name"
                android:screenOrientation="portrait"
                android:theme="@android:style/Theme.NoTitleBar" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.MAIN" /></p>
    <p>                <category android:name="android.intent.category.LAUNCHER" /><br />
                </intent-filter><br />
            </activity><br />
            <activity
                android:name="EditPreferences"
                android:screenOrientation="portrait"
                android:theme="@android:style/Theme.Black.NoTitleBar" ><br />
            </activity></p>
    <p>        <receiver android:name=".background.NotificationBarAlarm" /><br />
            <receiver
                android:name="on.boot.completed.AutoStart"
                android:enabled="true"
                android:exported="true" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.BOOT_COMPLETED" /><br />
                    <action android:name="android.intent.action.QUICKBOOT_POWERON" /><br />
                </intent-filter><br />
            </receiver></p>
    <p>        <service
                android:name=".background.StarterService"
                android:enabled="true"
                android:exported="true" /><br />
        </application></p>
    <p></manifest><br />
    

    A few notes about AndroidManifest.xml:

    • It’s very important to add
      <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

      before the <application> tag.

    • If the app is installed on the SD Card the autostart will not work! That’s why it’s important that we add
      android:installLocation="internalOnly"

      after package=“on.boot.completed“

    • Also add
      <action android:name="android.intent.action.QUICKBOOT_POWERON" />

      in addition to

      <action android:name="android.intent.action.BOOT_COMPLETED" />

      to the receiver. Some HTC devices won’t catch the BOOT_COMPLETED broadcast.

That’s all. Now build and run your app. After it has been started reboot your phone. The application’s notification service should start automatically after the device has booted up and display a message in the notification bar every 15 seconds.

Here you can download the working Android project:

Repeating AlarmManager after reboot example

Android SDK: Add application to the autostart

Add your Android application to the autostart

To add an Android application to the device’s autostart is actually pretty easy. But if some minor but important settings are wrong the autostart function doesn’t work. Here comes a fully functional example which opens your Android application after the device has finished its booting sequence.

  • Create a new Android application in Eclipse called „OnBootCompletedExample“. The namespace is on.boot.completed.
  • Leave the main Activity as is:

    OnBootCompletedExampleActivity.java

    <br />
    import android.app.Activity;<br />
    import android.os.Bundle;</p>
    <p>public class OnBootCompletedExampleActivity extends Activity {<br />
    	/** Called when the activity is first created. */<br />
    	@Override<br />
    	public void onCreate(Bundle savedInstanceState) {</p>
    <p>		super.onCreate(savedInstanceState);<br />
    		setContentView(R.layout.main);<br />
    	}<br />
    }<br />
    
  • Create a new class called Autostart.java which extends BroadcastReceiver:

    Autostart.java

    <br />
    import android.content.BroadcastReceiver;<br />
    import android.content.Context;<br />
    import android.content.Intent;<br />
    import android.util.Log;</p>
    <p>public class Autostart extends BroadcastReceiver {</p>
    <p>	/**<br />
    	 * Listens for Android's BOOT_COMPLETED broadcast and then executes<br />
    	 * the onReceive() method.<br />
    	 */<br />
    	@Override<br />
    	public void onReceive(Context context, Intent arg1) {<br />
    		Log.i("Autostart", "BOOT_COMPLETED broadcast received. Executing following code:");</p>
    <p>		Intent intent = new Intent(context, StarterService.class);<br />
    		context.startService(intent);<br />
    	}<br />
    }<br />
    

    In the AndroidManifest.xml file we are going to add this class as a receiver. This class will listen to the broadcast call the Android OS sends after the boot sequence has finished (meaning after the phone started up).

  • Create another class called StarterService.java which will extend Service:

    StarterService.java

    <br />
    import android.app.Service;<br />
    import android.content.Intent;<br />
    import android.os.IBinder;<br />
    import android.util.Log;<br />
    import android.widget.Toast;</p>
    <p>public class StarterService extends Service {<br />
    	private static final String TAG = "MyService";</p>
    <p>	@Override<br />
    	public IBinder onBind(Intent intent) {<br />
    		return null;<br />
    	}</p>
    <p>	@Override<br />
    	public void onDestroy() {<br />
    		Toast.makeText(this, "My Service stopped", Toast.LENGTH_LONG).show();<br />
    		Log.d(TAG, "onDestroy");<br />
    	}</p>
    <p>	/**<br />
    	 * The started service opens the Activity.<br />
    	 */<br />
    	@Override<br />
    	public void onStart(Intent intent, int startid) {<br />
    		Intent intents = new Intent(getBaseContext(), OnBootCompletedExampleActivity.class);<br />
    		intents.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);<br />
    		startActivity(intents);</p>
    <p>		Toast.makeText(this, "My Service started", Toast.LENGTH_LONG).show();<br />
    		Log.d(TAG, "onStart");<br />
    	}<br />
    }<br />
    

    When the class Autostart receives the BOOT_COMPLETED broadcast from Android OS it will start the StarterService which then starts the Android Activity „OnBootCompletedExampleActivity“.

  • And finally we update the AndroidManifest.xml with the changes we need to do in order for the application to work how we want:

    AndroidManifest.xml

    <br />
    <?xml version="1.0" encoding="utf-8"?><br />
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="on.boot.completed"
        android:installLocation="internalOnly"
        android:versionCode="1"
        android:versionName="1.0" ></p>
    <p>    <uses-sdk
            android:minSdkVersion="7"
            android:targetSdkVersion="7" /></p>
    <p>    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /></p>
    <p>    <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" ><br />
            <activity
                android:name="on.boot.completed.OnBootCompletedExampleActivity"
                android:label="@string/app_name"
                android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.MAIN" /></p>
    <p>                <category android:name="android.intent.category.LAUNCHER" /><br />
                </intent-filter><br />
            </activity></p>
    <p>        <receiver android:name="on.boot.completed.Autostart" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.QUICKBOOT_POWERON" /><br />
                    <action android:name="android.intent.action.BOOT_COMPLETED" /><br />
                </intent-filter><br />
            </receiver></p>
    <p>        <service
                android:name="on.boot.completed.StarterService"
                android:enabled="true"
                android:exported="true" /><br />
        </application></p>
    <p></manifest><br />
    

    A few notes about AndroidManifest.xml:

    • It’s very important to add
      <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

      before the <application> tag.

    • If the app is installed on the SD Card the autostart will not work! That’s why it’s important that we add
      android:installLocation="internalOnly"

      after package=“on.boot.completed“

    • Also add
      <action android:name="android.intent.action.QUICKBOOT_POWERON" />

      in addition to

      <action android:name="android.intent.action.BOOT_COMPLETED" />

      to the receiver. Some HTC devices won’t catch the BOOT_COMPLETED broadcast.

That’s all. Now build and run your app. After it has been started turn off your phone and turn it back on and the app should start automatically after the device has booted up.

Here you can download the working Android project:
OnBootCompletedExample

Unity 3D: Displaying a Fluct (Zucks AdNet) banner in a Unity Android game

Tutorial how to display an Fluct (Zucks AdNet) banner in an existing Unity Android project scene

Seems like using a simple Android JAR files inside a Unity Android project is not such a simple thing to do. I finally managed to get Zucks AdNet working in an existing Unity Android game. For this example I was using Unity for Windows version 3.5.2 and the latest Android plugin for Eclipse.

Prerequisites

I assume that you have a working installation of Eclipse with the Android plugin on your computer. If not, please follow this tutorial to get your workspace ready: Download the Android SDK.

The Eclipse/Android part

  • Register for an account at the Fluct website and download the ZucksAdnetSDK.jar file. In my example I am using version 1.1.3 of Fluct/ZucksAdnet.
  • Create a new Android project. Make sure that the namespace (the package name) is identical to the namespace of your Unity Android project. You can set the namespace in Unity through Build Settings > Player Settings > Android tab > Other Settings > „Bundle Identifier“. In my example I am using my.android.game.
  • Copy the ZucksAdnetSDK.jar file into the /libs folder of the project (you might have to create that folder manually).
  • Search inside of your Unity installation directory for the file classes.jar. Copy this file also into the /libs folder of your Eclipse project.
  • Create a class called ZucksUnityActivity.java. The class will look like this:

    <br />
    package my.android.game;</p>
    <p>import net.zucks.zucksAdnet.sdk.ZucksAdnetUserInfo;<br />
    import net.zucks.zucksAdnet.sdk.ZucksAdnetView;<br />
    import android.os.Bundle;<br />
    import android.view.Gravity;<br />
    import android.view.ViewGroup.LayoutParams;<br />
    import android.widget.LinearLayout;</p>
    <p>import com.unity3d.player.UnityPlayer;<br />
    import com.unity3d.player.UnityPlayerActivity;</p>
    <p>public class ZucksUnityActivity extends UnityPlayerActivity {</p>
    <p>	@Override<br />
    	public void onCreate(Bundle savedInstanceState) {<br />
    		super.onCreate(savedInstanceState);<br />
    	}</p>
    <p>	public static void showBanner() {</p>
    <p>		UnityPlayer.currentActivity.runOnUiThread(new Runnable() {<br />
    			public void run() {</p>
    <p>				ZucksAdnetUserInfo userInfo = ZucksAdnetUserInfo.getInstance(UnityPlayer.currentActivity.getApplicationContext());<br />
    				userInfo.setGender(0); // 0 = male, 1 = female<br />
    				userInfo.setAge(28);</p>
    <p>				// And this is the same, but done programmatically<br />
    				LinearLayout layout = new LinearLayout(UnityPlayer.currentActivity.getApplicationContext());<br />
    				layout.setOrientation(LinearLayout.VERTICAL);</p>
    <p>				// DEFINE HERE IF THE BANNER SHALL BE DISPLAYED ON TOP OR ON BOTTOM OF THE SCREEN<br />
    				layout.setGravity(Gravity.BOTTOM);<br />
    				UnityPlayer.currentActivity.addContentView(layout, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));</p>
    <p>				ZucksAdnetView zucksView = new ZucksAdnetView(UnityPlayer.currentActivity);<br />
    				layout.addView(zucksView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));<br />
    			}</p>
    <p>		});</p>
    <p>	}</p>
    <p>}<br />
    

    It’s important that you are using the ZucksAdnetUserInfo object. When I tried it without using this class the banner did not show up. The class extends UnityPlayerActivity instead of Activity. Also we created the static function showBanner() and left the onCreate() function nearly empty. Also we have to wrap the whole content of this function into

    <br />
    UnityPlayer.currentActivity.runOnUiThread(new Runnable() {<br />
    	public void run() {<br />
    		...<br />
    	}<br />
    }<br />
    

    otherwise we would get an error or a crash in Unity when we call the function. Also some content inside that function is a bit different in order to make it work with Unity.

  • Export the project to a JAR file. Click the right mouse button in the package explorer in Eclipse on your project and choose Export… > Java/JAR File > (standard settings) enter a name for the JAR file > Finish

Now you’re done with the part in Eclipse. Now we have to add that plugin into Unity.

The Unity part

  • Copy the created JAR file inside your Unity Android project into the folder /Plugins/Android/
  • Also copy the ZucksAdnetSDK.jar file into the same folder /Plugins/Android/
  • Complete the AndroidManifest.xml file inside your Unity Android project located at: \Assets\Plugins\Android\AndroidManifest.xml. The content of this file will look like this:
    <br />
    <?xml version="1.0" encoding="utf-8"?><br />
    <manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="my.android.game"
    	android:installLocation="preferExternal"
        android:versionCode="1"
        android:versionName="1.0"></p>
    <p>    <supports-screens
            android:smallScreens="true"
            android:normalScreens="true"
            android:largeScreens="true"
            android:xlargeScreens="true"
            android:anyDensity="true"/></p>
    <p>    <application
    		android:icon="@drawable/app_icon"
            android:label="@string/app_name"
            android:debuggable="true"></p>
    <p>		<!-- FLUCT/ZUCKS SDK --><br />
    		<meta-data android:name="MEDIA_ID" android:value="000000999"  /></p>
    <p>        <activity android:name="com.unity3d.player.UnityPlayerProxyActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity><br />
            <activity android:name="com.unity3d.player.UnityPlayerActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity><br />
            <activity android:name="com.unity3d.player.UnityPlayerNativeActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
                <meta-data android:name="android.app.lib_name" android:value="unity" /><br />
                <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" /><br />
            </activity><br />
            <activity android:name="com.unity3d.player.VideoPlayer"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity>	</p>
    <p>		<activity android:name="my.android.game.ZucksUnityActivity"></activity><br />
        </application></p>
    <p>	<!-- PERMISSIONS --><br />
    	<uses-permission android:name="android.permission.INTERNET"></uses-permission><br />
    	<uses-permission android:name="android.permission.GET_TASKS"></uses-permission><br />
    	<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission><br />
    	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission><br />
    	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission></p>
    <p></manifest><br />
    

    Replace the value of MEDIA_ID in the manifest file with the ID you received on the Fluct website when you registered. Also make sure the package name (namespace) is correct and identical to the namespace in your Eclipse project. Note that if you are already using other Android plugins this manifest file might be looking a bit different than in my example.

  • To finally display the ad banner inside a scene of your Unity Android game create or modify a C# script with the following content:
    <br />
    using UnityEngine;<br />
    using System.Collections;<br />
    using System.Collections.Generic;<br />
    using System.Runtime.InteropServices;<br />
    using System;</p>
    <p>public class Startup : MonoBehaviour<br />
    {<br />
    	public static AndroidJavaClass ZucksAdnetSDKJavaClass;</p>
    <p>	void Start() {</p>
    <p>		if( Application.platform == RuntimePlatform.Android ) {<br />
    			// Initialize Fluct/ZucksAdnetSDK<br />
    			ZucksAdnetSDKJavaClass = new AndroidJavaClass("my.android.game.ZucksUnityActivity");</p>
    <p>			// ZucksAdnetSDK display banner<br />
    			ZucksAdnetSDKJavaClass.CallStatic("showBanner");<br />
    		}</p>
    <p>	}<br />
    }<br />
    

    The banner will now be displayed either at the top or bottom of your phone’s screen depending on what you defined in the source code in the Eclipse project. Note that this code will only be executed on an actual Android device and the banner will not show up in the Unity player! „showBanner“ has to be identical to the function name you were using inside your Eclipse project.

  • If the Unity Android game crashes when you are trying to display a Fluct banner check the error log. If the error has anything to do with a missing database table „tbl_user_info“ then open the ZucksAdnetSDK.jar file with WinZip or WinRAR and copy all the *.sql files from the /assets folder to the /Plugins/Android/assets folder in your Unity Android project and try again.

In the end the screen will look similar like in this screenshot on the right side.

Well that’s all! I hope the tutorial helped you getting Smaad working in your game! Please leave your comments and questions below.

Unity 3D: Integrating Smaad SDK „More Games“ Android Activity in a Unity Android game

Tutorial on how to integrate Smaad SDK in a Unity Android game

This is a tutorial on how to implement Smaad SDK in a Unity Android project. Smaad SDK is some sort of a Japan-only equivalent of Tapjoy and also displays a full screen Activity with a list of „other apps“ for the user to check out. I’m using Smaad SDK for this example because I had to implement this for a customer project.

Prerequisites

I assume that you have a working installation of Eclipse with the Android plugin on your computer. If not, please follow this tutorial to get your workspace ready: Download the Android SDK.

The Eclipse/Android part

  • Register for an account at the Smaad website and download the Smaad JAR file.
  • Create a new Android project. Make sure that the namespace (the package name) is identical to the namespace of your Unity Android project. You can set the namespace in Unity through Build Settings > Player Settings > Android tab > Other Settings > „Bundle Identifier“. In my example I am using my.android.game.
  • Copy the Smaad JAR file (the name of it might be MoreApps.jar) into the /libs folder of the project (you might have to create that folder manually).
  • Search inside of your Unity installation directory for the file classes.jar. Copy this file also into the /libs folder of your Eclipse project.
  • To test if the ads are being displayed we first create a regular Android class called SmaadActivity.java. The content of this class looks like this:

    <br />
    package my.android.game;</p>
    <p>import jp.gmotech.MoreApps.MoreAppsActivity;<br />
    import android.app.Activity;<br />
    import android.content.Intent;<br />
    import android.os.Bundle;</p>
    <p>public class SmaadActivity extends Activity {</p>
    <p>	@Override<br />
    	public void onCreate(Bundle savedInstanceState) {<br />
    		super.onCreate(savedInstanceState);</p>
    <p>		Intent i = new Intent(getApplication(), MoreAppsActivity.class);<br />
    		i.putExtra("MoreAppsZoneId", "11111111"); // YOUR ZONEID HERE<br />
    		startActivity(i);<br />
    	}</p>
    <p>}<br />
    

    Make sure to change the value of „MoreAppsZoneId“ to your own ZoneId you received from the Smaad website.

  • Update the AndroidManifest.xml file that it looks like this:

    <br />
    <?xml version="1.0" encoding="utf-8"?><br />
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="my.android.game"
        android:versionCode="1"
        android:versionName="1.0" ></p>
    <p>    <uses-sdk android:minSdkVersion="7" /></p>
    <p>    <uses-permission android:name="android.permission.INTERNET" /><br />
        <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><br />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /></p>
    <p>    <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" ></p>
    <p>        <activity
                android:name="my.android.game.SmaadActivity"
                android:label="@string/app_name" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.MAIN" /></p>
    <p>                <category android:name="android.intent.category.LAUNCHER" /><br />
                </intent-filter><br />
            </activity><br />
            <activity
                android:name="jp.gmotech.MoreApps.MoreAppsActivity"
                android:configChanges="orientation"
                android:theme="@android:style/Theme.NoTitleBar" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.VIEW" /><br />
                    <category android:name="android.intent.category.DEFAULT" /><br />
                    <category android:name="android.intent.category.BROWSABLE" /><br />
                </intent-filter><br />
            </activity></p>
    <p>		<service android:name="jp.gmotech.MoreApps.MoreAppsIntentService" /><br />
        </application></p>
    <p></manifest><br />
    

    It’s important that you also add the activity „jp.gmotech.MoreApps.MoreAppsActivity“ to the manifest file since this is being used by Smaad.

  • Build and run the app on your Android phone and you should see the Smaad „more apps“ being displayed. If you don’t see this please refer to the official Smaad tutorials on how to implement this view into the Android app. Once this works continue with this tutorial.
  • Create a class called SmaadUnityActivity.java. You can use your previously created SmaadActivity.java class as a base. The class will look like this:

    <br />
    package my.android.game;</p>
    <p>import jp.gmotech.MoreApps.MoreAppsActivity;<br />
    import android.content.Intent;<br />
    import android.os.Bundle;</p>
    <p>import com.unity3d.player.UnityPlayer;<br />
    import com.unity3d.player.UnityPlayerActivity;</p>
    <p>public class SmaadUnityActivity extends UnityPlayerActivity {</p>
    <p>	@Override<br />
    	public void onCreate(Bundle savedInstanceState) {<br />
    		super.onCreate(savedInstanceState);<br />
    	}</p>
    <p>	public static void showSmaadMoreGamesScreen() {<br />
    		UnityPlayer.currentActivity.runOnUiThread(new Runnable() {<br />
    			public void run() {<br />
    				Intent intent = new Intent(UnityPlayer.currentActivity.getApplicationContext(), MoreAppsActivity.class);<br />
    				intent.putExtra("MoreAppsZoneId", "11111111"); // YOUR ZONEID HERE<br />
    				UnityPlayer.currentActivity.startActivity(intent);<br />
    			}</p>
    <p>		});<br />
    	}</p>
    <p>}<br />
    

    Make sure to change the value of „MoreAppsZoneId“ to your own ZoneId you received from the Smaad website. This class extends UnityPlayerActivity instead of Activity in order to make it work inside Unity. Also we created the static function showSmaadMoreGamesScreen() and left the onCreate() function nearly empty. To make it work inside Unity we have to wrap the whole content of the function showSmaadMoreGamesScreen() into these lines of code:

    <br />
    UnityPlayer.currentActivity.runOnUiThread(new Runnable() {<br />
    	public void run() {<br />
    		...<br />
    	}<br />
    }<br />
    

    If we don’t do this we would get an error or even a crash in Unity when we call this function. Also some content inside that function is a bit different in order to make it work with Unity. Please have a careful look at the above code.

  • Not entirely sure if the following step is needed but just do it anyways: Add this line to the AndroidManifest.xml file inside the „application“ tags:
    <br />
    <activity android:name="my.android.game.SmaadUnityActivity" android:screenOrientation="portrait" ></activity><br />
    
  • Export the project to a JAR file. Click the right mouse button in the package explorer in Eclipse on your project and choose Export… > Java/JAR File > (standard settings) enter a name for the JAR file > Finish

Now you’re done with the part in Eclipse. Next we have to add the created plugin into Unity.

The Unity part

  • Copy the created JAR file inside your Unity Android project into the folder /Plugins/Android/
  • Also copy the Smaad JAR (MoreApps.jar) file into the same folder /Plugins/Android/
  • Complete the AndroidManifest.xml file inside your Unity Android project located at: \Assets\Plugins\Android\AndroidManifest.xml. The content of this file will look like this:
    <br />
    <?xml version="1.0" encoding="utf-8"?><br />
    <manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="my.android.game"
    	android:installLocation="preferExternal"
        android:versionCode="1"
        android:versionName="1.0"><br />
        <supports-screens
            android:smallScreens="true"
            android:normalScreens="true"
            android:largeScreens="true"
            android:xlargeScreens="true"
            android:anyDensity="true"/></p>
    <p>    <application
    		android:icon="@drawable/app_icon"
            android:label="@string/app_name"
            android:debuggable="true"></p>
    <p>        <activity android:name="com.unity3d.player.UnityPlayerProxyActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity><br />
            <activity android:name="com.unity3d.player.UnityPlayerActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity><br />
            <activity android:name="com.unity3d.player.UnityPlayerNativeActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
                <meta-data android:name="android.app.lib_name" android:value="unity" /><br />
                <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" /><br />
            </activity><br />
            <activity android:name="com.unity3d.player.VideoPlayer"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity>	</p>
    <p>        <activity android:name="my.android.game.SmaadUnityActivity"></activity></p>
    <p>		<activity android:name="jp.gmotech.MoreApps.MoreAppsActivity" android:configChanges="orientation" android:theme="@android:style/Theme.NoTitleBar" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.VIEW" /><br />
                    <category android:name="android.intent.category.DEFAULT" /><br />
                    <category android:name="android.intent.category.BROWSABLE" /><br />
                </intent-filter><br />
            </activity></p>
    <p>        <service android:name="jp.gmotech.MoreApps.MoreAppsIntentService" /><br />
        </application></p>
    <p>	<!-- PERMISSIONS --><br />
    	<uses-permission android:name="android.permission.INTERNET"></uses-permission><br />
    	<uses-permission android:name="android.permission.GET_TASKS"></uses-permission><br />
    	<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission><br />
    	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission><br />
    	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission></p>
    <p></manifest></p>
    <p>

    Make sure the package name (namespace) is correct and identical to the namespace in your Eclipse project. Note that if you are already using other Android plugins this manifest file might be looking a bit different than in my example.

  • Finally we need to create a C# script in Unity to access that Android plugin:
    <br />
    using UnityEngine;<br />
    using System.Collections;<br />
    using System.Collections.Generic;<br />
    using System.Runtime.InteropServices;<br />
    using System;</p>
    <p>public class Startup : MonoBehaviour<br />
    {<br />
    	public static AndroidJavaClass smaadJavaClass;</p>
    <p>	void Start() {<br />
    		if( Application.platform == RuntimePlatform.Android ) {<br />
    			// Initialize Smaad<br />
    			smaadJavaClass = new AndroidJavaClass("my.android.game.SmaadUnityActivity");<br />
    		}<br />
    	}</p>
    <p>	void OnGUI () {<br />
    		if( Application.platform == RuntimePlatform.Android ) {<br />
    			if(GUI.Button(new Rect(10, 300, 150, 120), "Show SMAAD Screen")){<br />
    				smaadJavaClass.CallStatic("showSmaadMoreGamesScreen");<br />
    			}<br />
    		}<br />
    	}<br />
    }<br />
    

    Smaad SDK "More Games" activity

    For testing purposes we created a button inside the OnGUI() Unity function. If you press that button the Smaad Activity will be displayed. Note that this code will only be executed on an actual Android device and the button will not show up in the Unity player! „showSmaadMoreGamesScreen“ has to have the same name like the function you were using inside your Eclipse project.

In the end the screen will look similar like in this screenshot on the right side.

Well that’s all! I hope the tutorial helped you getting Smaad working in your game! Please leave your comments and questions below.

Unity 3D: Display an AdMob banner in an existing Unity Android game scene

Tutorial on how to display an AdMob banner in an existing Unity Android project scene

Seems like using a simple Android JAR files inside a Unity Android project is not such a simple thing to do. I finally managed to get AdMob working in an existing Unity Android game. For this example I was using Unity for Windows version 3.5.2 and the latest Android plugin for Eclipse.

Prerequisites

I assume that you have a working installation of Eclipse with the Android plugin on your computer. If not, please follow this tutorial to get your workspace ready: Download the Android SDK.

The Eclipse/Android part

  • Download the AdMob JAR file and register for an account if you haven’t done so already.
  • Create a new Android project. Make sure that the namespace (the package name) is identical to the namespace of your Unity Android project. You can set the namespace in Unity through Build Settings > Player Settings > Android tab > Other Settings > „Bundle Identifier“. In my example I am using my.android.game.
  • Copy the AdMob JAR file into the /libs folder of the project (you might have to create that folder manually).
  • Search inside of your Unity installation directory for the file classes.jar. Copy this file also into the /libs folder of your Eclipse project.
  • To test if the ads are being displayed we first create a regular Android class called AdMobActivity.java. The content of this class looks like this:

    <br />
    package my.android.game;</p>
    <p>import android.app.Activity;<br />
    import android.os.Bundle;<br />
    import android.util.Log;<br />
    import android.view.Gravity;<br />
    import android.view.ViewGroup.LayoutParams;<br />
    import android.widget.LinearLayout;</p>
    <p>import com.admob.android.ads.AdManager;<br />
    import com.admob.android.ads.AdView;<br />
    import com.admob.android.ads.SimpleAdListener;</p>
    <p>public class AdMobActivity extends Activity<br />
    {<br />
    	@Override<br />
    	protected void onCreate(Bundle savedInstanceState) {<br />
    		Log.i("AdMobTest", "onCreate");</p>
    <p>		// super (UnityPlayerActivity) will use setContentView() ...<br />
    		super.onCreate(savedInstanceState);</p>
    <p>		// ... so instead of using setContentView(), we call addContentView()<br />
    		// from setupAds()<br />
    		setupAds();</p>
    <p>		// Add test devices (check the logcat for the ID string of your<br />
    		// device..)<br />
    		// If you want to display test-ads uncomment this line of code and replace YOUR_DEVICE_ID with your device ID which AdMob displays in your logfile.<br />
    		AdManager.setTestDevices( new String[] { "YOUR_DEVICE_ID" } );<br />
    	}</p>
    <p>	private void setupAds() {<br />
    		// And this is the same, but done programmatically<br />
    		LinearLayout layout = new LinearLayout(this);<br />
    		layout.setOrientation(LinearLayout.VERTICAL);<br />
    		layout.setGravity(Gravity.BOTTOM);<br />
    		addContentView(layout, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));</p>
    <p>		AdView adView = new AdView(this);<br />
    		layout.addView(adView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));</p>
    <p>		adView.setBackgroundColor(0xff000000);<br />
    		adView.setPrimaryTextColor(0xffffffff);<br />
    		adView.setSecondaryTextColor(0xffcccccc);<br />
    		adView.setKeywords("Android game");<br />
    		adView.setRequestInterval(15);</p>
    <p>		// add listener for easier debugging<br />
    		adView.setAdListener(new SimpleAdListener() {<br />
    			@Override<br />
    			public void onFailedToReceiveAd(com.admob.android.ads.AdView adView) {<br />
    				Log.d("AdListener", "onFailedToReceiveAd: " + adView.toString());<br />
    				super.onFailedToReceiveAd(adView);<br />
    			}</p>
    <p>			@Override<br />
    			public void onFailedToReceiveRefreshedAd(com.admob.android.ads.AdView adView) {<br />
    				Log.d("AdListener", "onFailedToReceiveRefreshedAd: " + adView.toString());<br />
    				super.onFailedToReceiveRefreshedAd(adView);<br />
    			}</p>
    <p>			@Override<br />
    			public void onReceiveAd(com.admob.android.ads.AdView adView) {<br />
    				Log.d("AdListener", "onReceiveAd: " + adView.toString());<br />
    				super.onReceiveAd(adView);<br />
    			}</p>
    <p>			@Override<br />
    			public void onReceiveRefreshedAd(com.admob.android.ads.AdView adView) {<br />
    				Log.d("AdListener", "onReceiveRefreshedAd: " + adView.toString());<br />
    				super.onReceiveRefreshedAd(adView);<br />
    			}<br />
    		});</p>
    <p>		adView.requestFreshAd();<br />
    	}<br />
    }<br />
    
  • Update the AndroidManifest.xml file in your Eclipse project. It should look like this:

    <br />
    <?xml version="1.0" encoding="utf-8"?><br />
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="my.android.game"
        android:versionCode="1"
        android:versionName="1.0" ></p>
    <p>    <uses-sdk android:minSdkVersion="7" /></p>
    <p>    <uses-permission android:name="android.permission.INTERNET" /><br />
        <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><br />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /></p>
    <p>    <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" ><br />
            <activity
                android:name="my.android.game.AdMobActivity"
                android:label="@string/app_name" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.MAIN" /></p>
    <p>                <category android:name="android.intent.category.LAUNCHER" /><br />
                </intent-filter><br />
            </activity></p>
    <p> 		<!-- The application's publisher ID assigned by AdMob --><br />
            <meta-data android:value="YOUR_PUBLISHER_ID" android:name="ADMOB_PUBLISHER_ID" /></p>
    <p>        <!-- AdMobActivity definition --><br />
            <activity android:name="com.admob.android.ads.AdMobActivity"
                      android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
                      android:configChanges="orientation|keyboard|keyboardHidden" />        </p>
    <p>    </application></p>
    <p></manifest><br />
    

    Make sure to replace YOUR_PUBLISHER_ID with your actual AdMob publisher ID.

  • Build and run the app on your Android phone and you should see the AdMob banner being displayed. If you don’t please refer to the AdMob tutorials how to implement the banner into the Android app. Once this works continue with this tutorial.
  • Create a class called AdMobUnityActivity.java. You can use your previously created AdMobActivity.java class as a base. The class will look like this:

    <br />
    package my.android.game;</p>
    <p>import android.os.Bundle;<br />
    import android.util.Log;<br />
    import android.view.Gravity;<br />
    import android.view.ViewGroup.LayoutParams;<br />
    import android.widget.LinearLayout;</p>
    <p>import com.admob.android.ads.AdManager;<br />
    import com.admob.android.ads.AdView;<br />
    import com.admob.android.ads.SimpleAdListener;<br />
    import com.unity3d.player.UnityPlayer;<br />
    import com.unity3d.player.UnityPlayerActivity;</p>
    <p>public class AdMobUnityActivity extends UnityPlayerActivity {</p>
    <p>	@Override<br />
    	public void onCreate(Bundle savedInstanceState) {<br />
    		super.onCreate(savedInstanceState);<br />
    	}</p>
    <p>	public static void setupAdsStatic() {<br />
    		UnityPlayer.currentActivity.runOnUiThread(new Runnable() {<br />
    			public void run() {</p>
    <p>				// If you want to display test-ads uncomment this line of code and replace YOUR_DEVICE_ID with your device ID which AdMob displays in your logfile.<br />
    				// AdManager.setTestDevices(new String[] { "YOUR_DEVICE_ID" });</p>
    <p>				// And this is the same, but done programmatically<br />
    				LinearLayout layout = new LinearLayout(UnityPlayer.currentActivity.getApplicationContext());<br />
    				layout.setOrientation(LinearLayout.VERTICAL);</p>
    <p>				// SET HERE IF YOU WANT THE BANNER TO BE DISPLAYED AT THE TOP OR BOTTOM OF THE SCREEN<br />
    				layout.setGravity(Gravity.BOTTOM);<br />
    				UnityPlayer.currentActivity.addContentView(layout, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));</p>
    <p>				AdView adView = new AdView(UnityPlayer.currentActivity);<br />
    				layout.addView(adView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));</p>
    <p>				adView.setBackgroundColor(0xff000000);<br />
    				adView.setPrimaryTextColor(0xffffffff);<br />
    				adView.setSecondaryTextColor(0xffcccccc);</p>
    <p>				// SET SOME KEYWORDS FOR THE ADS TO DISPLAY<br />
    				adView.setKeywords("Android game");<br />
    				adView.setRequestInterval(15);</p>
    <p>				// add listener for easier debugging<br />
    				adView.setAdListener(new SimpleAdListener() {<br />
    					@Override<br />
    					public void onFailedToReceiveAd(com.admob.android.ads.AdView adView) {<br />
    						Log.d("AdListener", "onFailedToReceiveAd: " + adView.toString());<br />
    						super.onFailedToReceiveAd(adView);<br />
    					}</p>
    <p>					@Override<br />
    					public void onFailedToReceiveRefreshedAd(com.admob.android.ads.AdView adView) {<br />
    						Log.d("AdListener", "onFailedToReceiveRefreshedAd: " + adView.toString());<br />
    						super.onFailedToReceiveRefreshedAd(adView);<br />
    					}</p>
    <p>					@Override<br />
    					public void onReceiveAd(com.admob.android.ads.AdView adView) {<br />
    						Log.d("AdListener", "onReceiveAd: " + adView.toString());<br />
    						super.onReceiveAd(adView);<br />
    					}</p>
    <p>					@Override<br />
    					public void onReceiveRefreshedAd(com.admob.android.ads.AdView adView) {<br />
    						Log.d("AdListener", "onReceiveRefreshedAd: " + adView.toString());<br />
    						super.onReceiveRefreshedAd(adView);<br />
    					}<br />
    				});</p>
    <p>				adView.requestFreshAd();<br />
    			}</p>
    <p>		});</p>
    <p>	}<br />
    }<br />
    

    The class now extends UnityPlayerActivity instead of Activity. Also we created the static function setupAdsStatic() and left the onCreate() function nearly empty. Also we have to wrap the whole content of this function into

    <br />
    UnityPlayer.currentActivity.runOnUiThread(new Runnable() {<br />
    	public void run() {<br />
    		...<br />
    	}<br />
    }<br />
    

    otherwise we would get an error or a crash in Unity when we call the function. Also some content inside that function is a bit different in order to make it work with Unity.

  • Not entirely sure if the following step is needed but just do it anyways: Add this line to the AndroidManifest.xml file inside the „application“ tags:
    <br />
    <activity android:name="my.android.game.AdMobUnityActivity"></activity><br />
    
  • Export the project to a JAR file. Click the right mouse button in the package explorer in Eclipse on your project and choose Export… > Java/JAR File > (standard settings) enter a name for the JAR file > Finish

Now you’re done with the part in Eclipse. Now we have to add that plugin into Unity 3D.

The Unity part

  • Copy the created JAR file inside your Unity Android project into the folder /Plugins/Android/
  • Also copy the AdMob JAR file into the same folder /Plugins/Android/
  • Complete the AndroidManifest.xml file inside your Unity Android project located at: \Assets\Plugins\Android\AndroidManifest.xml. The content of this file will look like this:
    <br />
    <?xml version="1.0" encoding="utf-8"?><br />
    <manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="my.android.game"
    	android:installLocation="preferExternal"
        android:versionCode="1"
        android:versionName="1.0"></p>
    <p>    <supports-screens
            android:smallScreens="true"
            android:normalScreens="true"
            android:largeScreens="true"
            android:xlargeScreens="true"
            android:anyDensity="true"/></p>
    <p>    <application
    		android:icon="@drawable/app_icon"
            android:label="@string/app_name"
            android:debuggable="true"></p>
    <p>        <activity android:name="com.unity3d.player.UnityPlayerProxyActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity><br />
            <activity android:name="com.unity3d.player.UnityPlayerActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity><br />
            <activity android:name="com.unity3d.player.UnityPlayerNativeActivity"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
                <meta-data android:name="android.app.lib_name" android:value="unity" /><br />
                <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" /><br />
            </activity><br />
            <activity android:name="com.unity3d.player.VideoPlayer"
                      android:label="@string/app_name"
                      android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen"><br />
            </activity>	</p>
    <p>		<activity android:name="my.android.game.AdMobUnityActivity"></activity>	</p>
    <p>		<!-- The application's publisher ID assigned by AdMob --><br />
            <meta-data android:value="YOUR_PUBLISHER_ID" android:name="ADMOB_PUBLISHER_ID" /></p>
    <p>        <!-- AdMobActivity definition --><br />
            <activity android:name="com.admob.android.ads.AdMobActivity"
                      android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
                      android:configChanges="orientation|keyboard|keyboardHidden" />    </p>
    <p>    </application></p>
    <p>	<!-- PERMISSIONS --><br />
    	<uses-permission android:name="android.permission.INTERNET"></uses-permission><br />
    	<uses-permission android:name="android.permission.GET_TASKS"></uses-permission><br />
    	<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission><br />
    	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission><br />
    	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission></p>
    <p></manifest><br />
    

    Make sure the package name (namespace) is correct and identical to the namespace in your Eclipse project. Also change the YOUR_PUBLISHER_ID value with the value of your actual AdMob publisher ID. Note that if you are already using other Android plugins this manifest file might be looking a bit different than in my example.

  • To finally display the ad banner inside a scene of your Unity Android game create or modify a C# script with the following content:
    <br />
    using UnityEngine;<br />
    using System.Collections;<br />
    using System.Collections.Generic;<br />
    using System.Runtime.InteropServices;<br />
    using System;</p>
    <p>public class Startup : MonoBehaviour<br />
    {<br />
    	public static AndroidJavaClass adMobJavaClass;</p>
    <p>	void Start() {</p>
    <p>		if( Application.platform == RuntimePlatform.Android ) {<br />
    			// Initialize AdMob<br />
    			adMobJavaClass = new AndroidJavaClass("my.android.game.AdMobUnityActivity");</p>
    <p>			// AdMob display banner<br />
    			adMobJavaClass.CallStatic("setupAdsStatic");<br />
    		}</p>
    <p>	}<br />
    }<br />
    

    AdMob banner being displayed in Unity Android project

    The banner will now be displayed either at the top or bottom of your phone’s screen depending on what you defined in the source code in the Eclipse project. Note that this code will only be executed on an actual Android device and the banner will not show up in the Unity player! „setupAdsStatic“ has to be identical to the function name you were using inside your Eclipse project.

Well that’s all! I hope that helped you getting this nasty thing to work! Please leave your comments and questions below.

Saving & loading bitmaps to the Android device storage (internal & external)

In this tutorial I will show how to save a bitmap file to the external and internal storage of an Android device. It’s actually quite easy and only requires a few lines of code for the basic feature. As the source I am using a Bitmap object. If you want to use a image file from your drawable folder you can get it with this line of code:

Bitmap img = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);

If you want to save a thumbnail of an image you can easily change the size of the image with this line of code:

Bitmap thumbnail = Bitmap.createScaledBitmap(originalBitmap, width, height, false);

This will return a new Bitmap object with the desired width and height.

Saving the bitmap to the external storage

To save the Bitmap to the external storage (for example a SD card) use this code:

public final static String APP_PATH_SD_CARD = "/DesiredSubfolderName/";<br />
public final static String APP_THUMBNAIL_PATH_SD_CARD = "thumbnails";</p>
<p>public boolean saveImageToExternalStorage(Bitmap image) {<br />
String fullPath = Environment.getExternalStorageDirectory().getAbsolutePath() + APP_PATH_SD_CARD + APP_THUMBNAIL_PATH_SD_CARD;</p>
<p>try {<br />
File dir = new File(fullPath);<br />
if (!dir.exists()) {<br />
dir.mkdirs();<br />
}</p>
<p>OutputStream fOut = null;<br />
File file = new File(fullPath, "desiredFilename.png");<br />
file.createNewFile();<br />
fOut = new FileOutputStream(file);</p>
<p>// 100 means no compression, the lower you go, the stronger the compression<br />
image.compress(Bitmap.CompressFormat.PNG, 100, fOut);<br />
fOut.flush();<br />
fOut.close();</p>
<p>MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName());</p>
<p>return true;</p>
<p>} catch (Exception e) {<br />
Log.e("saveToExternalStorage()", e.getMessage());<br />
return false;<br />
}<br />
}</p>
<p>

Note that we have to pass the context to this variable. Otherwise we will not be able to use the getContentResolver() method. If you are using this method in an Activity class you can just delete „context“. Otherwise you can pass the context from an Activity class with „this“.

With the variable APP_PATH_SD_CARD you can set to which sub folder the file shall be saved. The root folder is the root folder of the SD card in the phone. You should name the sub folder with the name of your application so the user can easily find the proper folder of the saved files from your app.

Saving the bitmap to the internal storage

Alternatively you can also save the Bitmap to the internal storage in case the SD card is not available or for whatever other reasons you may have. Files saved to the internal storage are only accessible by the application which saved the files. Neither the user nor other applications can access those files. Also, all these files will be deleted once the application was uninstalled by the user.

public boolean saveImageToInternalStorage(Bitmap image) {</p>
<p>try {<br />
// Use the compress method on the Bitmap object to write image to<br />
// the OutputStream<br />
FileOutputStream fos = context.openFileOutput("desiredFilename.png", Context.MODE_PRIVATE);</p>
<p>// Writing the bitmap to the output stream<br />
image.compress(Bitmap.CompressFormat.PNG, 100, fos);<br />
fos.close();</p>
<p>return true;<br />
} catch (Exception e) {<br />
Log.e("saveToInternalStorage()", e.getMessage());<br />
return false;<br />
}<br />
}</p>
<p>

Note that we have to pass the context to this variable. Otherwise we will not be able to use the openFileOutput() method. If you are using this method in an Activity class you can just delete „context“. Otherwise you can pass the context from an Activity class with „this“.

You can change the value of Context.MODE_PRIVATE to Context.MODE_WORLD_READABLE so that other applications will be able to access those files.

 Reading a bitmap from the storage

With this method you can read a file from the storage. The method first checks for the file in the external storage. If the file is not there or there is no external storage it will look for the file in the internal storage.

First I made a method which checks if the external storage is readable.

public boolean isSdReadable() {</p>
<p>boolean mExternalStorageAvailable = false;<br />
String state = Environment.getExternalStorageState();</p>
<p>if (Environment.MEDIA_MOUNTED.equals(state)) {<br />
// We can read and write the media<br />
mExternalStorageAvailable = true;<br />
Log.i("isSdReadable", "External storage card is readable.");<br />
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {<br />
// We can only read the media<br />
Log.i("isSdReadable", "External storage card is readable.");<br />
mExternalStorageAvailable = true;<br />
} else {<br />
// Something else is wrong. It may be one of many other<br />
// states, but all we need to know is we can neither read nor write<br />
mExternalStorageAvailable = false;<br />
}</p>
<p>return mExternalStorageAvailable;<br />
}</p>
<p>

We will be using the above method in this following method:

public Bitmap getThumbnail(String filename) {</p>
<p>String fullPath = Environment.getExternalStorageDirectory().getAbsolutePath() + APP_PATH_SD_CARD + APP_THUMBNAIL_PATH_SD_CARD;<br />
Bitmap thumbnail = null;</p>
<p>// Look for the file on the external storage<br />
try {<br />
if (tools.isSdReadable() == true) {<br />
thumbnail = BitmapFactory.decodeFile(fullPath + "/" + filename);<br />
}<br />
} catch (Exception e) {<br />
Log.e("getThumbnail() on external storage", e.getMessage());<br />
}</p>
<p>// If no file on external storage, look in internal storage<br />
if (thumbnail == null) {<br />
try {<br />
File filePath = context.getFileStreamPath(filename);<br />
FileInputStream fi = new FileInputStream(filePath);<br />
thumbnail = BitmapFactory.decodeStream(fi);<br />
} catch (Exception ex) {<br />
Log.e("getThumbnail() on internal storage", ex.getMessage());<br />
}<br />
}<br />
return thumbnail;<br />
}</p>
<p>

Note that we have to pass the context to this variable. Otherwise we will not be able to use the getFileStreamPath() method. If you are using this method in an Activity class you can just delete „context“. Otherwise you can pass the context from an Activity class with „this“.

So basically this method only needs the filename and returns a Bitmap object of this image for further use. If you want to use this Bitmap object for your ImageView in your activity you can set the bitmap to this object with this line of code:

myImageView.setImageBitmap(MyBitmapObject);

 

 

Unity 3D: Using Java classes/code

With Unity 3D you can use Java code. Here is a simple example how to use Java classes/code with Unity. Note that this tutorial only works with managed DLL files! If you want to use unmanaged DLL files you best have a look at the tutorial on the Unity 3D page here.

Create the Java class

First we create a Java class for testing:

<br />
package android.unity.plugin;</p>
<p>public class MyJava {</p>
<p>public MyJava() {<br />
}</p>
<p>public String sayTokyo() {<br />
return "Tokyo";<br />
}</p>
<p>}<br />

Export it to JAR

Export this class to a JAR file. You can do this with Eclipse in the package explorer by clicking the right mouse button on the class and choose Export -> JAR File. You do notneed to set any special options. Just define your desired dll file name and click export.

Convert it to DLL

Next go to IKVM.NET. Download and install this software. With IKVM you can convert JAR files into DLL files. This is what we need to do in order to be able to use the Java code with Unity.

Make sure to add the /bin path of IKVM to your System Environment Variable. It will look something like this:
C:\Users\de\Development\ikvm-7.0.4335.0\bin

Open a command prompt (CMD) and go to the directory where the previously exported JAR file is located. Then enter following command to convert the JAR file to a DLL file:

<br />
C:\Users\de>cd Desktop<br />
C:\Users\de\Desktop>ikvmc -out:MyJava.dll MyJava.jar<br />
C:\Users\de\Desktop><br />

When this command has been executed you have the MyJava.dll file which now can be used in Unity (this DLL file is a so called „managed dll“ file).

The Unity 3D stuff

Open Unity and import the MyJava.dll to your Assets folder. I copied mine to the Plugins sub folder but as far as I know this is not necessary.

I am not sure if this step is necessary but I did it anyways: Copy all the .dll files from the IKVM folder to the Assets/Plugins folder in Unity.

Create a .cs script – MyManagedJavaPlugin.cs – with Unity with the following content:

<br />
using UnityEngine;<br />
using android.unity.plugin;</p>
<p>public class MyManagedJavaPlugin : MonoBehaviour {</p>
<p>void Awake() {<br />
MyJava obj = new MyJava();<br />
print(obj.sayTokyo());<br />
}</p>
<p>}<br />

android.unity.plugin has to be the namespace you used when you created the Java project with Eclipse.

Next you can attach the MyManagedJavaPlugin.cs script to an object in your Unity scene. Hit the play button in Unity and you will see that when the „Awake“ action is happening in Unity the sayTokyo() method will be called. The string „Tokyo“ will then be output in your Unity console.

That’s it for this tutorial. From now on you can modify your script and classes to fit your needs.

Nächste Seite »