Slide 1

Slide 1 text

Deep Dive into Slices 森 洋之@Yahoo! JAPAN

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

goo.gl/fW2YPh

Slide 5

Slide 5 text

Slice

Slide 6

Slide 6 text

youtu.be/a7IVH5aNwwc

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

テンプレート化されたUI

Slide 9

Slide 9 text

テンプレート化されたUI

Slide 10

Slide 10 text

インタラクティブ

Slide 11

Slide 11 text

アップデート可能

Slide 12

Slide 12 text

Sliceの仕組み

Slide 13

Slide 13 text

URIをもとにSliceを要求 ContentProvider

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

実装方法

Slide 16

Slide 16 text

一発です!

Slide 17

Slide 17 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 18

Slide 18 text

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

Slide 19

Slide 19 text

どこで使われるか

Slide 20

Slide 20 text

Google Search

Slide 21

Slide 21 text

1. by App Name

Slide 22

Slide 22 text

Step1: meta-dataを追加 ... ... …

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

2. by General Terms

Slide 25

Slide 25 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 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

3. 3rd party to 3rd party

Slide 29

Slide 29 text

Step1: 続報を待つ

Slide 30

Slide 30 text

開放されなかったら?

Slide 31

Slide 31 text

自分で作る

Slide 32

Slide 32 text

Sliceの具体的な仕組み

Slide 33

Slide 33 text

goo.gl/pfiqiv SliceViewer

Slide 34

Slide 34 text

スライスの要求

Slide 35

Slide 35 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() } … }

Slide 36

Slide 36 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 37

Slide 37 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); ... } ...

Slide 38

Slide 38 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 39

Slide 39 text

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

Slide 40

Slide 40 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 41

Slide 41 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を 取得し ContentProviderClient #call()を呼び 結果をSliceに変換する

Slide 42

Slide 42 text

ContentProviderClient

Slide 43

Slide 43 text

ContentProviderClient ・ い ・めんどくさい ・スレッドセーフじゃない

Slide 44

Slide 44 text

ContentProviderClient#call

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

スライスを返す

Slide 47

Slide 47 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を作成し Bundleに変換し Bundleに詰めて返す

Slide 48

Slide 48 text

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

Slide 49

Slide 49 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秒 重たい処理マジおこ onBindSlice()

Slide 50

Slide 50 text

スライスの表示

Slide 51

Slide 51 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 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

まとめ

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

おしまい