Skip to content

Commit

Permalink
Include sticker support.
Browse files Browse the repository at this point in the history
Gboard now supports indexing of stickers via Firebase App Indexing.
This change adds code that demonstrates how to index stickers.

Change-Id: I07ed92db1c69847ce023680e5da08186e2ed9e13
  • Loading branch information
kroikie committed Aug 24, 2017
1 parent 9defa33 commit d224414
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 15 deletions.
1 change: 1 addition & 0 deletions app-indexing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Getting Started
- Open the App Indexing project in Android Studio.
- Run the sample on your Android device or emulator by issuing the following command using **adb** tool:
`adb shell am start -a android.intent.action.VIEW -d "https://www.example.com/articles/test" com.google.samples.quickstart.app_indexing`
- Use ADD STICKERS and CLEAR STICKERS buttons to add and remove stickers from the index.

Result
-----------
Expand Down
23 changes: 23 additions & 0 deletions app-indexing/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,32 @@
android:pathPrefix="/articles/" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mystickers" />
<data android:host="sticker" />
</intent-filter>

</activity>
<!-- [END intent_filter] -->

<provider
android:name="com.google.samples.quickstart.app_indexing.StickerProvider"
android:authorities="com.google.samples.quickstart.app_indexing.StickerProvider"
android:exported="true"
android:grantUriPermissions="true">
</provider>

<service android:name=".AppIndexingService"
android:exported="true"
android:permission="com.google.android.gms.permission.APPINDEXING">
<intent-filter>
<action android:name="com.google.firebase.appindexing.UPDATE_INDEX" />
</intent-filter>
</service>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.google.samples.quickstart.app_indexing;

import android.app.IntentService;
import android.content.Intent;

import com.google.firebase.appindexing.FirebaseAppIndex;

public class AppIndexingService extends IntentService {

public AppIndexingService() {
super("AppIndexingService");
}

@Override
protected void onHandleIntent(Intent intent) {
AppIndexingUtil.setStickers(getApplicationContext(), FirebaseAppIndex.getInstance());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package com.google.samples.quickstart.app_indexing;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log;
import android.widget.Toast;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.appindexing.FirebaseAppIndex;
import com.google.firebase.appindexing.FirebaseAppIndexingInvalidArgumentException;
import com.google.firebase.appindexing.Indexable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* See firebase app indexing api code lab
* https://codelabs.developers.google.com/codelabs/app-indexing/#0
*/

public class AppIndexingUtil {
private static final String STICKER_FILENAME_PATTERN = "sticker%s.png";
private static final String CONTENT_URI_ROOT =
String.format("content://%s/", StickerProvider.class.getName());
private static final String STICKER_URL_PATTERN = "mystickers://sticker/%s";
private static final String STICKER_PACK_URL_PATTERN = "mystickers://sticker/pack/%s";
private static final String CONTENT_PROVIDER_STICKER_PACK_NAME = "Local Content Pack";
private static final String TAG = "AppIndexingUtil";
public static final String FAILED_TO_CLEAR_STICKERS = "Failed to clear stickers";
public static final String FAILED_TO_INSTALL_STICKERS = "Failed to install stickers";

public static void clearStickers(final Context context, FirebaseAppIndex firebaseAppIndex) {
Task<Void> task = firebaseAppIndex.removeAll();

task.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Toast.makeText(context, "Successfully cleared stickers", Toast.LENGTH_SHORT).show();
}
});
task.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, FAILED_TO_CLEAR_STICKERS, e);
Toast.makeText(context, FAILED_TO_CLEAR_STICKERS, Toast.LENGTH_SHORT).show();
}
});
}

public static void setStickers(final Context context, FirebaseAppIndex firebaseAppIndex) {
try {
List<Indexable> stickers = getIndexableStickers(context);
Indexable stickerPack = getIndexableStickerPack(context, stickers);

List<Indexable> indexables = new ArrayList<>(stickers);
indexables.add(stickerPack);

Task<Void> task = firebaseAppIndex.update(
indexables.toArray(new Indexable[indexables.size()]));

task.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Toast.makeText(context, "Successfully added stickers", Toast.LENGTH_SHORT)
.show();
}
});

task.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.d(TAG, FAILED_TO_INSTALL_STICKERS, e);
Toast.makeText(context, FAILED_TO_INSTALL_STICKERS, Toast.LENGTH_SHORT)
.show();
}
});
} catch (IOException | FirebaseAppIndexingInvalidArgumentException e) {
Log.e(TAG, "Unable to set stickers", e);
}
}

private static Indexable getIndexableStickerPack(Context context, List<Indexable> stickers)
throws IOException, FirebaseAppIndexingInvalidArgumentException {
Indexable.Builder indexableBuilder = getIndexableBuilder(context, Color.CYAN,
STICKER_PACK_URL_PATTERN, stickers.size());
indexableBuilder.put("hasSticker", stickers.toArray(new Indexable[stickers.size()]));
return indexableBuilder.build();
}

private static List<Indexable> getIndexableStickers(Context context) throws IOException,
FirebaseAppIndexingInvalidArgumentException {
List<Indexable> indexableStickers = new ArrayList<>();
int[] stickerColors = new int[] {Color.GREEN, Color.RED, Color.BLUE,
Color.YELLOW, Color.MAGENTA};

for (int i = 0; i < stickerColors.length; i++) {
Indexable.Builder indexableStickerBuilder = getIndexableBuilder(context,
stickerColors[i], STICKER_URL_PATTERN, i);
indexableStickerBuilder.put("keywords", "tag1_" + i, "tag2_" + i)
// StickerPack object that the sticker is part of.
.put("partOf", new Indexable.Builder("StickerPack")
.setName(CONTENT_PROVIDER_STICKER_PACK_NAME)
.build());
indexableStickers.add(indexableStickerBuilder.build());
}

return indexableStickers;
}

private static Indexable.Builder getIndexableBuilder(Context context, int color,
String urlPattern, int index)
throws IOException {
File stickersDir = new File(context.getFilesDir(), "stickers");

if (!stickersDir.exists() && !stickersDir.mkdirs()) {
throw new IOException("Stickers directory does not exist");
}

String filename = String.format(STICKER_FILENAME_PATTERN, index);
File stickerFile = new File(stickersDir, filename);

writeSolidColorBitmapToFile(stickerFile, color);

Uri contentUri = Uri.parse(CONTENT_URI_ROOT + filename);
String url = String.format(urlPattern, index);

Indexable.Builder indexableBuilder = new Indexable.Builder("StickerPack")
// name of the sticker pack
.setName(CONTENT_PROVIDER_STICKER_PACK_NAME)
// Firebase App Indexing unique key that must match an intent-filter
// (e.g. mystickers://stickers/pack/0)
.setUrl(url)
// (Optional) - Defaults to the first sticker in "hasSticker"
// displayed as a category image to select between sticker packs that should
// be representative of the sticker pack
.setImage(contentUri.toString())
// (Optional) - Defaults to a generic phrase
// content description of the image that is used for accessibility
// (e.g. TalkBack)
.setDescription("Indexable description");

return indexableBuilder;
}

/**
* Writes a simple bitmap to local storage. The image is a solid color with size 400x400
*/
private static void writeSolidColorBitmapToFile(File file, int color) throws IOException {
Bitmap bitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888);
bitmap.eraseColor(color);

FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
} finally {
if (fos != null) {
fos.close();
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.google.android.gms.tasks.OnFailureListener;
Expand All @@ -47,6 +49,23 @@ protected void onCreate(Bundle savedInstanceState) {
// [START_EXCLUDE]
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

final FirebaseAppIndex firebaseAppIndex = FirebaseAppIndex.getInstance();

Button addStickersBtn = findViewById(R.id.addStickersBtn);
addStickersBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startService(new Intent(MainActivity.this, AppIndexingService.class));
}
});
Button clearStickersBtn = findViewById(R.id.clearStickersBtn);
clearStickersBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AppIndexingUtil.clearStickers(MainActivity.this, firebaseAppIndex);
}
});
// [END_EXCLUDE]
onNewIntent(getIntent());
}
Expand All @@ -56,7 +75,7 @@ protected void onNewIntent(Intent intent) {
Uri data = intent.getData();
if (Intent.ACTION_VIEW.equals(action) && data != null) {
articleId = data.getLastPathSegment();
TextView linkText = (TextView)findViewById(R.id.link);
TextView linkText = findViewById(R.id.link);
linkText.setText(data.toString());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.google.samples.quickstart.app_indexing;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
* Provider that makes the stickers queryable by other applications.
*/
public class StickerProvider extends ContentProvider {
@Nullable private File mRootDir;

@Override
public boolean onCreate() {
final Context context = getContext();
if (context != null) {
mRootDir = new File(context.getFilesDir(), "stickers");
try {
mRootDir = mRootDir.getCanonicalFile();
} catch (IOException e) {
mRootDir = null;
}
}
return mRootDir != null;
}

@Nullable
@Override
public String getType(@NonNull Uri uri) {
final File file = uriToFile(uri);
if (!isFileInRoot(file)) {
throw new SecurityException("File is not is root: " + file);
}
return getMimeType(file);
}

@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
final File file = uriToFile(uri);
if (!isFileInRoot(file)) {
throw new SecurityException("File is not is root: " + file);
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}

private File uriToFile(@NonNull Uri uri) {
if (mRootDir == null) {
throw new IllegalStateException("Root directory is null");
}
File file = new File(mRootDir, uri.getEncodedPath());
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to get canonical file: " + file);
}
return file;
}

private boolean isFileInRoot(@NonNull File file) {
return mRootDir != null && file.getPath().startsWith(mRootDir.getPath());
}

private String getMimeType(@NonNull File file) {
String mimeType = null;
final String extension = getFileExtension(file);
if (extension != null) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
if (mimeType == null) {
mimeType = "application/octet-stream";
}
return mimeType;
}

@Nullable
private String getFileExtension(@NonNull File file) {
String extension = null;
final String filename = file.getName();
final int index = filename.lastIndexOf('.');
if (index >= 0) {
extension = filename.substring(index + 1);
}
return extension;
}

@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder) {
throw new UnsupportedOperationException("no queries");
}

@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
throw new UnsupportedOperationException("no inserts");
}

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

@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
@Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("no updates");
}
}
Loading

0 comments on commit d224414

Please sign in to comment.