Slide 1

Slide 1 text

Deep Dive into Slices 森 洋之@Yahoo! JAPAN

Slide 2

Slide 2 text

森 洋之 @moridroid

Slide 3

Slide 3 text

森 洋之 @moridroid Yahoo! JAPAN Androidアプリ黒帯  (こういうところで話す仕事)

Slide 4

Slide 4 text

森 洋之 @moridroid Yahoo! JAPAN Androidアプリ黒帯  (こういうところで話す仕事) ヤフオク!のアプリ担当

Slide 5

Slide 5 text

goo.gl/fW2YPh

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

いまここ

Slide 8

Slide 8 text

いまここ 啓文堂書店

Slide 9

Slide 9 text

はじめます

Slide 10

Slide 10 text

Slice

Slide 11

Slide 11 text

dependencies { // ... implementation "androidx.slice:slice-core:1.0.0-alpha1" implementation "androidx.slice:slice-builders:1.0.0-alpha1" // ... } Jetpackに含まれるコンポーネント

Slide 12

Slide 12 text

テンプレート化されたUI

Slide 13

Slide 13 text

インタラクティブ

Slide 14

Slide 14 text

アップデート可能

Slide 15

Slide 15 text

Sliceの仕組み

Slide 16

Slide 16 text

これを作る

Slide 17

Slide 17 text

ContentProvider

Slide 18

Slide 18 text

URIをもとにSliceを要求 ContentProvider

Slide 19

Slide 19 text

slice { slice (horizontal, list_item) { slice (list_item) { Icon(...) (no_tint), “Mon” (title), “62°”, }, slice (list_item) { … }, ... } } UIを構造化されたデータで表現

Slide 20

Slide 20 text

実装方法

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

一発です!

Slide 26

Slide 26 text

もうちょっと

Slide 27

Slide 27 text

Step1: SliceProviderをつくる class MySliceProvider : SliceProvider() { ... override fun onBindSlice(sliceUri: Uri): Slice? { val context = context ?: return null return if (sliceUri.path == "/") { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("URI found.") } .build() } else { ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("URI not found.") } .build() } } ... }

Slide 28

Slide 28 text

Step1: SliceProviderをつくる class MySliceProvider : SliceProvider() { ... override fun onBindSlice(sliceUri: Uri): Slice? { val context = context ?: return null return if (sliceUri.path == "/") { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("URI found.") } .build() } else { ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("URI not found.") } .build() } } ... }

Slide 29

Slide 29 text

Step1: SliceProviderをつくる class MySliceProvider : SliceProvider() { ... override fun onBindSlice(sliceUri: Uri): Slice? { val context = context ?: return null return if (sliceUri.path == "/") { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("URI found.") } .build() } else { ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("URI not found.") } .build() } } ... }

Slide 30

Slide 30 text

Step2: マニフェストに追加する ...

Slide 31

Slide 31 text

どこで使われるか

Slide 32

Slide 32 text

Google Search

Slide 33

Slide 33 text

1. by App Name

Slide 34

Slide 34 text

Step1: meta-dataを追加 ... ... … メタデータ追加

Slide 35

Slide 35 text

Step2: パーミッションを追加 val manager = SliceManager.getInstance(this) manager.grantSlicePermission( “com.google.android.googlequicksearchbox”, mainUri) manager.grantSlicePermission( “com.google.android.gms”, mainUri) パーミッション要求

Slide 36

Slide 36 text

2. by General Terms

Slide 37

Slide 37 text

val appIndex = FirebaseAppIndex.getInstance() appIndex.update(Indexable.Builder() .setUrl(contentUrl) .setName(“Temperature”) .setKeywords(“living room”, “kitchen”, “bedroom”) .setMetadata(Indexable.Metadata.Builder() .setSliceUri(sliceUri)) .build()) Step1: Indexを追加 メタデータ追加

Slide 38

Slide 38 text

Step2: intent-filterを追加 ... ... ... intent-filter追加

Slide 39

Slide 39 text

class MySliceProvider : SliceProvider() { … override fun onMapIntentToUri(intent: Intent?): Uri { val path = intent.data.path.replace(“/slice”, “”) return Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(“com.example.slicesample”) .path(path) .build() } … } Step3: App IndexingのURIを SliceProviderのURIに変換する onMapIntentToUriを オーバーライド

Slide 40

Slide 40 text

class MySliceProvider : SliceProvider() { … override fun onMapIntentToUri(intent: Intent?): Uri { val path = intent.data.path.replace(“/slice”, “”) return Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(“com.example.slicesample”) .path(path) .build() } … } Step3: App IndexingのURIを SliceProviderのURIに変換する AppIndexingのURIを

Slide 41

Slide 41 text

class MySliceProvider : SliceProvider() { … override fun onMapIntentToUri(intent: Intent?): Uri { val path = intent.data.path.replace(“/slice”, “”) return Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(“com.example.slicesample”) .path(path) .build() } … } Step3: App IndexingのURIを SliceProviderのURIに変換する Slice用のURIに変換

Slide 42

Slide 42 text

3rd partyは?

Slide 43

Slide 43 text

3rd party to 3rd party

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

Step1: 続報を待つ

Slide 47

Slide 47 text

開放されなかったら?

Slide 48

Slide 48 text

自分で作る

Slide 49

Slide 49 text

Sliceの具体的な仕組み

Slide 50

Slide 50 text

goo.gl/pfiqiv SliceViewer

Slide 51

Slide 51 text

スライスの要求

Slide 52

Slide 52 text

class SingleSliceViewerActivity : AppCompatActivity() { … private fun bindSlice(uri: Uri) { sliceView.bind( context = this, lifecycleOwner = this, uri = uri, scrollable = true ) viewModel.selectedMode.observe(this, Observer { sliceView.mode = it ?: SliceView.MODE_LARGE }) uriValue.text = uri.toString() } … } bind開始

Slide 53

Slide 53 text

fun SliceView.bind( context: Context, lifecycleOwner: LifecycleOwner, uri: Uri, ... ) { ... val intent = Intent(Intent.ACTION_VIEW, uri) val sliceLiveData = SliceLiveData.fromIntent(context, intent) sliceLiveData?.removeObservers(lifecycleOwner) try { sliceLiveData?.observe(lifecycleOwner, Observer { updatedSlice -> … } SliceLiveData 生成

Slide 54

Slide 54 text

fun SliceView.bind( context: Context, lifecycleOwner: LifecycleOwner, uri: Uri, ... ) { ... val intent = Intent(Intent.ACTION_VIEW, uri) val sliceLiveData = SliceLiveData.fromIntent(context, intent) sliceLiveData?.removeObservers(lifecycleOwner) try { sliceLiveData?.observe(lifecycleOwner, Observer { updatedSlice -> … } 値を監視

Slide 55

Slide 55 text

public final class SliceLiveData { ... private static class SliceLiveDataImpl extends LiveData { private SliceLiveDataImpl(Context context, Uri uri) { super(); mSliceManager = SliceManager.getInstance(context); ... } @Override protected void onActive() { AsyncTask.execute(mUpdateSlice); ... } ... SliceManager 取得

Slide 56

Slide 56 text

private final Runnable mUpdateSlice = new Runnable() { @Override public void run() { Slice s = mUri != null ? mSliceManager.bindSlice(mUri) : mSliceManager.bindSlice(mIntent); ... } }; ... } public final class SliceLiveData { ... private static class SliceLiveDataImpl extends LiveData { private SliceLiveDataImpl(Context context, Uri uri) { super(); mSliceManager = SliceManager.getInstance(context); ... } @Override protected void onActive() { AsyncTask.execute(mUpdateSlice); ... } ... bindSlice() を呼ぶ

Slide 57

Slide 57 text

public abstract class SliceManager { /** * Get a {@link SliceManager}. */ @SuppressWarnings("NewApi") public static @NonNull SliceManager getInstance(@NonNull Context context) { if (BuildCompat.isAtLeastP()) { return new SliceManagerWrapper(context); } else { return new SliceManagerCompat(context); } } … }

Slide 58

Slide 58 text

public abstract class SliceManager { /** * Get a {@link SliceManager}. */ @SuppressWarnings("NewApi") public static @NonNull SliceManager getInstance(@NonNull Context context) { if (BuildCompat.isAtLeastP()) { return new SliceManagerWrapper(context); } else { return new SliceManagerCompat(context); } } … } P以降

Slide 59

Slide 59 text

public abstract class SliceManager { /** * Get a {@link SliceManager}. */ @SuppressWarnings("NewApi") public static @NonNull SliceManager getInstance(@NonNull Context context) { if (BuildCompat.isAtLeastP()) { return new SliceManagerWrapper(context); } else { return new SliceManagerCompat(context); } } … } なかでSystemService 取得してる P以降

Slide 60

Slide 60 text

public abstract class SliceManager { /** * Get a {@link SliceManager}. */ @SuppressWarnings("NewApi") public static @NonNull SliceManager getInstance(@NonNull Context context) { if (BuildCompat.isAtLeastP()) { return new SliceManagerWrapper(context); } else { return new SliceManagerCompat(context); } } … } P未満

Slide 61

Slide 61 text

public abstract class SliceManager { /** * Get a {@link SliceManager}. */ @SuppressWarnings("NewApi") public static @NonNull SliceManager getInstance(@NonNull Context context) { if (BuildCompat.isAtLeastP()) { return new SliceManagerWrapper(context); } else { return new SliceManagerCompat(context); } } … } P未満 今日はこっち

Slide 62

Slide 62 text

class SliceManagerCompat extends SliceManagerBase { … @Override public Slice bindSlice(@NonNull Intent intent) { return SliceProviderCompat.bindSlice(mContext, intent, SUPPORTED_SPECS); } … } SliceProviderCompat呼ぶだけ

Slide 63

Slide 63 text

class SliceManagerCompat extends SliceManagerBase { … @Override public Slice bindSlice(@NonNull Intent intent) { return SliceProviderCompat.bindSlice(mContext, intent, SUPPORTED_SPECS); } … } ※Sliceを返す側も、  SliceProviderCompatを使います

Slide 64

Slide 64 text

つまり

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

public class SliceProviderCompat { … public static Slice bindSlice(Context context, Intent intent, Set supportedSpecs) { ... ContentResolver resolver = context.getContentResolver(); ... // Check if the intent has data for the slice uri on it and use that final Uri intentData = intent.getData(); if (intentData != null && SLICE_TYPE.equals(resolver.getType(intentData))) { return bindSlice(context, intentData, supportedSpecs); } … } … }

Slide 68

Slide 68 text

public class SliceProviderCompat { … public static Slice bindSlice(Context context, Uri uri, Set supportedSpecs) { ProviderHolder holder = acquireClient(context.getContentResolver(), uri); ... try { ... final Bundle res = holder.mProvider.call(METHOD_SLICE, null, extras); ... return new Slice((Bundle) bundle); } … }

Slide 69

Slide 69 text

public class SliceProviderCompat { … public static Slice bindSlice(Context context, Uri uri, Set supportedSpecs) { ProviderHolder holder = acquireClient(context.getContentResolver(), uri); ... try { ... final Bundle res = holder.mProvider.call(METHOD_SLICE, null, extras); ... return new Slice((Bundle) bundle); } … } ContentProviderClient 取得

Slide 70

Slide 70 text

public class SliceProviderCompat { … public static Slice bindSlice(Context context, Uri uri, Set supportedSpecs) { ProviderHolder holder = acquireClient(context.getContentResolver(), uri); ... try { ... final Bundle res = holder.mProvider.call(METHOD_SLICE, null, extras); ... return new Slice((Bundle) bundle); } … } ContentProviderClient#call() を呼び

Slide 71

Slide 71 text

public class SliceProviderCompat { … public static Slice bindSlice(Context context, Uri uri, Set supportedSpecs) { ProviderHolder holder = acquireClient(context.getContentResolver(), uri); ... try { ... final Bundle res = holder.mProvider.call(METHOD_SLICE, null, extras); ... return new Slice((Bundle) bundle); } … } Sliceに変換

Slide 72

Slide 72 text

ContentProvider

Slide 73

Slide 73 text

ContentProviderClient

Slide 74

Slide 74 text

ContentProviderClient #call( )

Slide 75

Slide 75 text

ContentProviderClient

Slide 76

Slide 76 text

ContentProviderClient#call

Slide 77

Slide 77 text

ContentProviderClient#call

Slide 78

Slide 78 text

ContentProviderClient#call ・ContentProviderの任意のメソッドを呼ぶ  のではなく  ContentProvider#call()を呼ぶ

Slide 79

Slide 79 text

ContentProviderClient#call ・ContentProviderの任意のメソッドを呼ぶ  のではなく  ContentProvider#call()を呼ぶ ・ContentProviderは、  引数のmethodで判定して、なんかして、  結果はBundleで返す

Slide 80

Slide 80 text

スライスを返す

Slide 81

Slide 81 text

public class SliceProviderCompat { … public Bundle call(String method, String arg, Bundle extras) { if (method.equals(METHOD_SLICE)) { ... Slice s = handleBindSlice(uri, specs, getCallingPackage()); Bundle b = new Bundle(); b.putParcelable(EXTRA_SLICE, s != null ? s.toBundle() : null); return b; } else if (method.equals(METHOD_MAP_INTENT)) { … } … }

Slide 82

Slide 82 text

public class SliceProviderCompat { … public Bundle call(String method, String arg, Bundle extras) { if (method.equals(METHOD_SLICE)) { ... Slice s = handleBindSlice(uri, specs, getCallingPackage()); Bundle b = new Bundle(); b.putParcelable(EXTRA_SLICE, s != null ? s.toBundle() : null); return b; } else if (method.equals(METHOD_MAP_INTENT)) { … } … } Sliceを生成

Slide 83

Slide 83 text

public class SliceProviderCompat { … public Bundle call(String method, String arg, Bundle extras) { if (method.equals(METHOD_SLICE)) { ... Slice s = handleBindSlice(uri, specs, getCallingPackage()); Bundle b = new Bundle(); b.putParcelable(EXTRA_SLICE, s != null ? s.toBundle() : null); return b; } else if (method.equals(METHOD_MAP_INTENT)) { … } … } Bundleに変換

Slide 84

Slide 84 text

public class SliceProviderCompat { … public Bundle call(String method, String arg, Bundle extras) { if (method.equals(METHOD_SLICE)) { ... Slice s = handleBindSlice(uri, specs, getCallingPackage()); Bundle b = new Bundle(); b.putParcelable(EXTRA_SLICE, s != null ? s.toBundle() : null); return b; } else if (method.equals(METHOD_MAP_INTENT)) { … } … } Bundleに積む

Slide 85

Slide 85 text

public class SliceProviderCompat { … private Slice handleBindSlice(final Uri sliceUri, final Set specs, final String callingPkg) { ... return onBindSliceStrict(sliceUri, specs); } … }

Slide 86

Slide 86 text

public class SliceProviderCompat { … private Slice onBindSliceStrict(Uri sliceUri, Set specs) { … mHandler.postDelayed(mAnr, SLICE_BIND_ANR); try { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyDeath() .build()); ... try { return mProvider.onBindSlice(sliceUri); } ...

Slide 87

Slide 87 text

public class SliceProviderCompat { … private Slice onBindSliceStrict(Uri sliceUri, Set specs) { … mHandler.postDelayed(mAnr, SLICE_BIND_ANR); try { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyDeath() .build()); ... try { return mProvider.onBindSlice(sliceUri); } ... ANR 2秒

Slide 88

Slide 88 text

public class SliceProviderCompat { … private Slice onBindSliceStrict(Uri sliceUri, Set specs) { … mHandler.postDelayed(mAnr, SLICE_BIND_ANR); try { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyDeath() .build()); ... try { return mProvider.onBindSlice(sliceUri); } ... 重たい処理 マジおこ

Slide 89

Slide 89 text

public class SliceProviderCompat { … private Slice onBindSliceStrict(Uri sliceUri, Set specs) { … mHandler.postDelayed(mAnr, SLICE_BIND_ANR); try { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyDeath() .build()); ... try { return mProvider.onBindSlice(sliceUri); } ... SliceProvider #onBindSlice()

Slide 90

Slide 90 text

スライスの表示

Slide 91

Slide 91 text

public final class SliceLiveData { ... private static class SliceLiveDataImpl extends LiveData { … private final Runnable mUpdateSlice = new Runnable() { @Override public void run() { Slice s = mUri != null ? mSliceManager.bindSlice(mUri) : mSliceManager.bindSlice(mIntent); ... postValue(s); } }; ... }

Slide 92

Slide 92 text

fun SliceView.bind( context: Context, lifecycleOwner: LifecycleOwner, uri: Uri, ... ) { ... try { sliceLiveData?.observe(lifecycleOwner, Observer { updatedSlice -> if (updatedSlice == null) return@Observer slice = updatedSlice ... }) …

Slide 93

Slide 93 text

public class SliceView extends ViewGroup implements Observer, View.OnClickListener { … public void setSlice(@Nullable Slice slice) { ... mCurrentSlice = slice; reinflate(); } … } SliceからViewを生成 ※省略

Slide 94

Slide 94 text

Sliceめいたものを つくるなら

Slide 95

Slide 95 text

・Bundleと相互変換できるクラスHogeをつくる  ※めんどくさい ・ContentProviderClientを使って、  高速にBundleをやりとりする ・Hoge→Viewのロジックを書く※めんどくさい

Slide 96

Slide 96 text

・Bundleと相互変換できるクラスHogeをつくる  ※めんどくさい ・ContentProviderClientを使って、  高速にBundleをやりとりする ・Hoge→Viewのロジックを書く※めんどくさい

Slide 97

Slide 97 text

・Bundleと相互変換できるクラスHogeをつくる  ※めんどくさい ・ContentProviderClientを使って、  高速にBundleをやりとりする ・Hoge→Viewのロジックを書く※めんどくさい

Slide 98

Slide 98 text

まとめ

Slide 99

Slide 99 text

・Sliceは古典的なAPIを上手に使っている ・最初はGoogle検索アプリで使われる ・サードパーティでも表示できるかは不透明 ・でも似たようなことは自前でできるね!

Slide 100

Slide 100 text

・Sliceは古典的な技術を上手に使っている ・最初はGoogle検索アプリで使われる ・サードパーティでも表示できるかは不透明 ・でも似たようなことは自前でできるね!

Slide 101

Slide 101 text

・Sliceは古典的な技術を上手に使っている ・最初はGoogle検索アプリで使われる ・サードパーティでも表示できるかは不透明 ・でも似たようなことは自前でできるね!

Slide 102

Slide 102 text

・Sliceは古典的な技術を上手に使っている ・最初はGoogle検索アプリで使われる ・サードパーティでも表示できるかは不透明 ・でも似たようなことは自前でできるね!

Slide 103

Slide 103 text

・Sliceは古典的な技術を上手に使っている ・最初はGoogle検索アプリで使われる ・サードパーティでも表示できるかは不透明 ・でも似たようなことは自前でできるね! ・本を書いたのでよろしくお願いします

Slide 104

Slide 104 text

おしまい