diff --git a/.gitignore b/.gitignore index 520a863..95c85f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,55 +1,20 @@ -# Built application files -*.apk -*.ap_ - -# Files for the ART/Dalvik VM -*.dex - -# Java class files -*.class - # Generated files bin/ gen/ -out/ # Gradle files .gradle/ build/ +/*/build/ # Local configuration file (sdk path, etc) local.properties -# Proguard folder generated by Eclipse -proguard/ - # Log Files *.log -# Android Studio Navigation editor temp files -.navigation/ - -# Android Studio captures folder -captures/ - -# Intellij -*.iml -.idea/workspace.xml -.idea/tasks.xml -.idea/gradle.xml -.idea/dictionaries -.idea/libraries - -# Keystore files -*.jks - -# External native build folder generated in Android Studio 2.2 and later -.externalNativeBuild - -# Google Services (e.g. APIs or Firebase) -google-services.json - -# Freeline -freeline.py -freeline/ -freeline_project_description.json +.gradle +.idea +.DS_Store +/captures +*.iml \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..b146702 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,36 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 'android-P' + defaultConfig { + applicationId "ru.touchin.checknewsupport" + minSdkVersion 24 + targetSdkVersion 'P' + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:appcompat-v7:28.0.0-alpha1' + implementation "com.android.support:design:28.0.0-alpha1" + implementation "com.android.support:recyclerview-selection:28.0.0-alpha1" + implementation 'com.android.support:slices-view:28.0.0-alpha1' + implementation 'com.android.support:slices-builders:28.0.0-alpha1' + implementation 'com.android.support.constraint:constraint-layout:1.1.0-beta5' + implementation 'androidx.core:core-ktx:0.2' +} + +repositories { + mavenCentral() +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fe91f06 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/BottomAppBarActivity.kt b/app/src/main/java/ru/touchin/checknewsupport/BottomAppBarActivity.kt new file mode 100644 index 0000000..b6b8cfb --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/BottomAppBarActivity.kt @@ -0,0 +1,29 @@ +package ru.touchin.checknewsupport + +import android.os.Bundle +import android.support.design.bottomappbar.BottomAppBar +import android.support.v7.app.AppCompatActivity +import android.view.View + +class BottomAppBarActivity : AppCompatActivity() { + + private lateinit var bottomAppBar: BottomAppBar + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_bottom_app_bar) + bottomAppBar = findViewById(R.id.bottom_appbar) + + //setSupportActionBar(bottom_appbar) calling this breaks it! + bottomAppBar.replaceMenu(R.menu.bottom_app_menu) + findViewById(R.id.toggle_alignment).setOnClickListener { + bottomAppBar.toggleAlignment() + } + } + + private fun BottomAppBar.toggleAlignment() { + val current = fabAlignmentMode + fabAlignmentMode = current.xor(1) + } + +} diff --git a/app/src/main/java/ru/touchin/checknewsupport/ChipActivity.kt b/app/src/main/java/ru/touchin/checknewsupport/ChipActivity.kt new file mode 100644 index 0000000..80d787f --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/ChipActivity.kt @@ -0,0 +1,65 @@ +package ru.touchin.checknewsupport + +import android.os.Bundle +import android.support.design.chip.Chip +import android.support.design.chip.ChipGroup +import android.support.v4.content.ContextCompat +import android.support.v7.app.AppCompatActivity +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.EditText + +class ChipActivity : AppCompatActivity() { + + private lateinit var chipGroupSingleLine: ChipGroup + private lateinit var chipGroupMultiLine: ChipGroup + private lateinit var chipGroupSingleLineBlock: View + private lateinit var newChipTextEdit: EditText + private lateinit var newChipAddButton: View + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_chip) + + chipGroupMultiLine = findViewById(R.id.chip_group_multiple_line) + newChipTextEdit = findViewById(R.id.add_chip_text_field) + newChipAddButton = findViewById(R.id.add_chip_button) + chipGroupSingleLine = findViewById(R.id.chip_group_single_line) + chipGroupSingleLineBlock = findViewById(R.id.chip_group_single_line_block) + + newChipAddButton.setOnClickListener { + chipGroupMultiLine.addView(createChip()) + chipGroupSingleLine.addView(createChip()) + newChipTextEdit.setText("") + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + return when (item?.itemId) { + R.id.change_group_visibility -> { + changeGroupVisibility() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.bottom_app_menu, menu) + return true + } + + private fun changeGroupVisibility() { + val oldVisibility = chipGroupSingleLineBlock.visibility + chipGroupSingleLineBlock.visibility = chipGroupMultiLine.visibility + chipGroupMultiLine.visibility = oldVisibility + } + + private fun createChip() = Chip(this).apply { + chipText = newChipTextEdit.text + isCheckable = true + isClickable = true + chipBackgroundColor = ContextCompat.getColorStateList(context, R.color.colorAccent) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/MainActivity.kt b/app/src/main/java/ru/touchin/checknewsupport/MainActivity.kt new file mode 100644 index 0000000..35f6564 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/MainActivity.kt @@ -0,0 +1,29 @@ +package ru.touchin.checknewsupport + +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.view.View +import ru.touchin.checknewsupport.selection.SelectionActivity +import ru.touchin.checknewsupport.slices.SliceActivity + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_main) + + findViewById(R.id.open_recycler_view_selection).setOnClickListener { openNewActivity(SelectionActivity::class.java) } + findViewById(R.id.open_material_elements).setOnClickListener { openNewActivity(MaterialElementsActivity::class.java) } + findViewById(R.id.open_chip).setOnClickListener { openNewActivity(ChipActivity::class.java) } + findViewById(R.id.open_bottom_app_navigation).setOnClickListener { openNewActivity(BottomAppBarActivity::class.java) } + findViewById(R.id.open_slice).setOnClickListener { openNewActivity(SliceActivity::class.java) } + + } + + private fun openNewActivity(cls: Class<*>) { + startActivity(Intent(this, cls)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/MaterialElementsActivity.kt b/app/src/main/java/ru/touchin/checknewsupport/MaterialElementsActivity.kt new file mode 100644 index 0000000..e1891c1 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/MaterialElementsActivity.kt @@ -0,0 +1,12 @@ +package ru.touchin.checknewsupport + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity + +class MaterialElementsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_material) + } +} diff --git a/app/src/main/java/ru/touchin/checknewsupport/selection/ActionModeController.kt b/app/src/main/java/ru/touchin/checknewsupport/selection/ActionModeController.kt new file mode 100644 index 0000000..03c9448 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/selection/ActionModeController.kt @@ -0,0 +1,33 @@ +package ru.touchin.checknewsupport.selection + +import android.support.v7.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import androidx.recyclerview.selection.SelectionTracker +import ru.touchin.checknewsupport.R + +class ActionModeController( + private val tracker: SelectionTracker<*> +) : ActionMode.Callback { + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.action_menu, menu) + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + tracker.clearSelection() + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = true + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = when (item.itemId) { + R.id.action_clear -> { + mode.finish() + true + } + else -> false + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/selection/SelectionActivity.kt b/app/src/main/java/ru/touchin/checknewsupport/selection/SelectionActivity.kt new file mode 100644 index 0000000..a29939f --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/selection/SelectionActivity.kt @@ -0,0 +1,68 @@ +package ru.touchin.checknewsupport.selection + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.view.ActionMode +import android.support.v7.widget.DividerItemDecoration +import android.support.v7.widget.RecyclerView +import androidx.recyclerview.selection.SelectionTracker +import androidx.recyclerview.selection.StorageStrategy +import ru.touchin.checknewsupport.R +import ru.touchin.checknewsupport.selection.models.Word +import ru.touchin.checknewsupport.selection.words.WordAdapter +import ru.touchin.checknewsupport.selection.words.WordKeyProvider +import ru.touchin.checknewsupport.selection.words.WordLookup + +class SelectionActivity : AppCompatActivity() { + + private lateinit var recyclerView: RecyclerView + private var actionMode: ActionMode? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_selection) + + recyclerView = findViewById(R.id.recycler_view) + + val items = IntRange(0, 100).map { Word(it, it.toString()) } + val adapter = WordAdapter(items) + + recyclerView.adapter = adapter + recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + + val tracker = SelectionTracker + .Builder( + // индетифицируем трекер в констексе + "someId", + recyclerView, + // для Long ItemKeyProvider реализован в виде StableIdKeyProvider + WordKeyProvider(items), + WordLookup(recyclerView), + // существуют аналогичные реализации для Long и String + StorageStrategy.createParcelableStorage(Word::class.java) + ) + .build() + + adapter.tracker = tracker + + tracker.addObserver(object : SelectionTracker.SelectionObserver() { + override fun onSelectionChanged() { + super.onSelectionChanged() + if (tracker.hasSelection() && actionMode == null) { + actionMode = startSupportActionMode(ActionModeController(tracker)) + setSelectedTitle(tracker.selection.size()) + } else if (!tracker.hasSelection()) { + actionMode?.finish() + actionMode = null + } else { + setSelectedTitle(tracker.selection.size()) + } + } + }) + } + + private fun setSelectedTitle(selected: Int) { + actionMode?.title = "Selected: $selected" + } + +} diff --git a/app/src/main/java/ru/touchin/checknewsupport/selection/ViewHolderWithDetails.kt b/app/src/main/java/ru/touchin/checknewsupport/selection/ViewHolderWithDetails.kt new file mode 100644 index 0000000..04134b5 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/selection/ViewHolderWithDetails.kt @@ -0,0 +1,9 @@ +package ru.touchin.checknewsupport.selection + +import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails + +interface ViewHolderWithDetails { + + fun getItemDetail(): ItemDetails + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/selection/models/Word.kt b/app/src/main/java/ru/touchin/checknewsupport/selection/models/Word.kt new file mode 100644 index 0000000..48e0bcf --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/selection/models/Word.kt @@ -0,0 +1,30 @@ +package ru.touchin.checknewsupport.selection.models + +import android.os.Parcel +import android.os.Parcelable + +data class Word(val id: Int, val text: String) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readInt(), + parcel.readString()) { + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(id) + parcel.writeString(text) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Word { + return Word(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordAdapter.kt b/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordAdapter.kt new file mode 100644 index 0000000..a62baf2 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordAdapter.kt @@ -0,0 +1,53 @@ +package ru.touchin.checknewsupport.selection.words + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.selection.SelectionTracker +import ru.touchin.checknewsupport.R +import ru.touchin.checknewsupport.selection.ViewHolderWithDetails +import ru.touchin.checknewsupport.selection.models.Word +import ru.touchin.checknewsupport.selection.words.WordAdapter.WordViewHolder + +class WordAdapter(private val items: List) : RecyclerView.Adapter() { + + lateinit var tracker: SelectionTracker + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = WordViewHolder( + LayoutInflater + .from(parent.context) + .inflate(R.layout.item_word, parent, false), items + ) + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: WordViewHolder, position: Int) = Unit + + override fun onBindViewHolder(holder: WordViewHolder, position: Int, payloads: List) { + holder.setActivatedState(tracker.isSelected(items[position])) + + if (SelectionTracker.SELECTION_CHANGED_MARKER !in payloads) { + holder.changeText(items[position]) + } + + } + + class WordViewHolder(itemView: View, private val items: List) : RecyclerView.ViewHolder(itemView), ViewHolderWithDetails { + + private val text: TextView = itemView.findViewById(R.id.item_word_text) + + override fun getItemDetail() = WordDetails(adapterPosition, items.getOrNull(adapterPosition)) + + fun setActivatedState(isActivated: Boolean) { + itemView.isActivated = isActivated + } + + fun changeText(word: Word) { + text.text = word.text + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordDetails.kt b/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordDetails.kt new file mode 100644 index 0000000..6c85194 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordDetails.kt @@ -0,0 +1,12 @@ +package ru.touchin.checknewsupport.selection.words + +import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails +import ru.touchin.checknewsupport.selection.models.Word + +class WordDetails(private val adapterPosition: Int, private val selectedKey: Word?) : ItemDetails() { + + override fun getSelectionKey() = selectedKey + + override fun getPosition() = adapterPosition + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordKeyProvider.kt b/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordKeyProvider.kt new file mode 100644 index 0000000..f2b7971 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordKeyProvider.kt @@ -0,0 +1,15 @@ +package ru.touchin.checknewsupport.selection.words + +import androidx.recyclerview.selection.ItemKeyProvider +import ru.touchin.checknewsupport.selection.models.Word + +// В конструкторе ItemKeyProvider мы выбираем метод предоставления доступа к данным: +// SCOPE_MAPPED - ко всем данным. Позволяет реализовать Shift+click выбор данных +// SCOPE_CACHED - к данным, которые были недавно или сейчас на экране. Экономит память +class WordKeyProvider(private val items: List) : ItemKeyProvider(ItemKeyProvider.SCOPE_CACHED) { + + override fun getKey(position: Int) = items.getOrNull(position) + + override fun getPosition(key: Word) = items.indexOf(key) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordLookup.kt b/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordLookup.kt new file mode 100644 index 0000000..1fb62ae --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/selection/words/WordLookup.kt @@ -0,0 +1,16 @@ +package ru.touchin.checknewsupport.selection.words + +import android.support.v7.widget.RecyclerView +import android.view.MotionEvent +import androidx.recyclerview.selection.ItemDetailsLookup +import ru.touchin.checknewsupport.selection.ViewHolderWithDetails +import ru.touchin.checknewsupport.selection.models.Word + +class WordLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() { + + override fun getItemDetails(e: MotionEvent) = recyclerView.findChildViewUnder(e.x, e.y) + ?.let { + (recyclerView.getChildViewHolder(it) as? ViewHolderWithDetails)?.getItemDetail() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/checknewsupport/slices/ProviderDetector.kt b/app/src/main/java/ru/touchin/checknewsupport/slices/ProviderDetector.kt new file mode 100644 index 0000000..b476d5e --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/slices/ProviderDetector.kt @@ -0,0 +1,35 @@ +package ru.touchin.checknewsupport.slices + +import android.content.Context +import android.content.DialogInterface +import android.content.pm.PackageManager +import android.net.Uri +import android.support.v7.app.AlertDialog +import ru.touchin.checknewsupport.R + +internal fun providerAppNotInstalled(packageManager: PackageManager, providerAuthority: String): Boolean { + try { + val packageInfo = packageManager.getPackageInfo(providerAuthority, PackageManager.GET_PROVIDERS) + ?: return true + + val contentProvider = packageInfo.providers.find { it.authority == providerAuthority } + return contentProvider == null + } catch (e: PackageManager.NameNotFoundException) { + return true + } +} + +fun showMissingProviderDialog(context: Context, onDismiss: () -> Unit, sliceUri: Uri) { + AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog_Alert) + .setMessage("""Content provider not found: $sliceUri. + + • Does the URI start with content:// (e.g., for authority "com.example", it's content://com.example)? + + • Does the provider authority match its app's package name? + + • Have you installed the slice provider app first? + """) + .setPositiveButton("OK", { dialog: DialogInterface, _ -> dialog.dismiss() }) + .setOnDismissListener { onDismiss.invoke() } + .show() +} diff --git a/app/src/main/java/ru/touchin/checknewsupport/slices/SliceActivity.kt b/app/src/main/java/ru/touchin/checknewsupport/slices/SliceActivity.kt new file mode 100644 index 0000000..5425ef2 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/slices/SliceActivity.kt @@ -0,0 +1,121 @@ +package ru.touchin.checknewsupport.slices + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.support.constraint.ConstraintLayout +import android.support.constraint.ConstraintSet +import android.support.v7.app.AppCompatActivity +import android.transition.TransitionManager +import android.view.View +import androidx.slice.SliceManager +import androidx.slice.widget.SliceView +import ru.touchin.checknewsupport.R + +class SliceActivity : AppCompatActivity() { + + private val baseSliceUri: Uri = Uri.parse("content://ru.touchin.provider/") + private val timeSliceUri = baseSliceUri.buildUpon().appendPath("time").build() + + private lateinit var sliceView: SliceView + private lateinit var getPermissionsButton: View + private lateinit var sliceContainer: ConstraintLayout + + private lateinit var sliceManager: SliceManager + private var pendingSliceUri: Uri? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_slice_host) + + sliceView = findViewById(R.id.slice_view) + getPermissionsButton = findViewById(R.id.button_grant_permission) + sliceContainer = findViewById(R.id.slice_container) + sliceManager = SliceManager.getInstance(this) + + findViewById(R.id.get_slice).setOnClickListener { + tryShowingSlice(timeSliceUri) + } + + transitionUiTo(UiState.Empty, animate = false) + } + + override fun onStart() { + super.onStart() + + if (providerAppNotInstalled(packageManager, baseSliceUri.authority)) { + showMissingProviderDialog(this, { finish() }, baseSliceUri) + return + } + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + handleSlicePermissionActivityResult(requestCode, onSlicePermissionResult = { + if (pendingSliceUri == null) { + throw IllegalArgumentException("The slice URI to bind to cannot be null") + } + tryShowingSlice(pendingSliceUri!!) + pendingSliceUri = null + }) + } + + private fun tryShowingSlice(sliceUri: Uri) { + if (sliceManager.missingPermission(sliceUri, appName = getString(R.string.app_name))) { + transitionUiTo(UiState.NeedPermission) + getPermissionsButton.setOnClickListener { + pendingSliceUri = sliceUri + sliceManager.requestPermission(sliceUri, this) + } + } else { + getSliceAndBind(sliceUri) + } + } + + private fun getSliceAndBind(sliceUri: Uri) { + transitionUiTo(UiState.SliceContent) + sliceView.setSlice(sliceManager.bindSlice(sliceUri)) + + // TODO due to a bug in 28.0.0-alpha1, we can't use the LiveData yet 😭 +// SliceLiveData.fromUri(this, sliceUri) +// .observe(this, Observer({ sliceResult -> +// sliceView.setSlice(sliceResult) +// invalidateOptionsMenu() +// })) + } + + private fun transitionUiTo(uiState: UiState, animate: Boolean = true) { + if (animate) { + TransitionManager.beginDelayedTransition(sliceContainer) + } + + ConstraintSet().apply { + clone(sliceContainer) + setVisibility(R.id.group_need_permission, uiState.permissionGroupVisibility) + setVisibility(R.id.slice_view, uiState.sliceViewVisibility) + applyTo(sliceContainer) + } + } + + private sealed class UiState { + + abstract val permissionGroupVisibility: Int + + abstract val sliceViewVisibility: Int + + object Empty : UiState() { + override val permissionGroupVisibility = View.INVISIBLE + override val sliceViewVisibility = View.INVISIBLE + } + + object NeedPermission : UiState() { + override val permissionGroupVisibility = View.VISIBLE + override val sliceViewVisibility = View.INVISIBLE + } + + object SliceContent : UiState() { + override val permissionGroupVisibility = View.INVISIBLE + override val sliceViewVisibility = View.VISIBLE + } + } +} diff --git a/app/src/main/java/ru/touchin/checknewsupport/slices/SlicePermissions.kt b/app/src/main/java/ru/touchin/checknewsupport/slices/SlicePermissions.kt new file mode 100644 index 0000000..a1ac5c3 --- /dev/null +++ b/app/src/main/java/ru/touchin/checknewsupport/slices/SlicePermissions.kt @@ -0,0 +1,76 @@ +package ru.touchin.checknewsupport.slices + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.PendingIntent +import android.content.IntentSender +import android.net.Uri +import android.util.Log +import android.widget.Toast +import androidx.slice.Slice +import androidx.slice.SliceManager + +internal fun SliceManager.missingPermission(sliceUri: Uri, appName: String) = try { + // TODO this is a very expensive and hacky way to check, but we don't really have alternatives + // as Android P DP1, since there's no permission API 🤷‍ + bindSlice(sliceUri)?.isPermissionRequest(appName) == true +} catch (e: SecurityException) { + true +} catch (e: IllegalArgumentException) { + false +} + +@SuppressLint("InlinedApi") // It's fine, the constants are used by the support library +private fun Slice.isPermissionRequest(appName: String): Boolean { + // Permission slices have only one item, whose action is a PendingIntent to + // show the permission granting activity (which shows a dialog). We cannot + // easily check the contents of the action PendingIntent, so for now we just + // assume that if it's not null it's good enough. We'll check other signals + // afterwards to verify if it's really a permission slice. + if (items.count() != 1 || items[0].action == null) { + return false + } + + // Permission slices contain one slice item, which contains a slice item which is + // a FORMAT_TEXT with a fixed format + val subItems = items.first().slice.items + val firstSubItem = subItems.firstOrNull() + return firstSubItem?.format == android.app.slice.SliceItem.FORMAT_TEXT && + firstSubItem.text.contains("$appName wants to show Provider slices") +} + +internal fun SliceManager.requestPermission(sliceUri: Uri, activity: Activity) { + try { + val intentSender: IntentSender? = bindSlice(sliceUri)!!.permissionRequestPendingIntent.intentSender + activity.startIntentSenderForResult(intentSender, REQUEST_CODE_PERMISSIONS, null, 0, 0, 0) + } catch (e: IllegalArgumentException) { + Toast.makeText( + activity, + "Looks like we've hit a bug in Slices, fixed in alpha2. " + + "Uninstall the app and reboot the device/emulator.", + Toast.LENGTH_LONG + ).show() + } +} + +private val Slice.permissionRequestPendingIntent: PendingIntent + get() = try { + items[0].action + } catch (e: ClassCastException) { + Log.e("Slice&Dice", "Trying to extract permission request pending intent from a slice which is not a permission request: $this") + throw IllegalArgumentException("The slice '$this' is not a valid permission request slice.") + } + +internal inline fun handleSlicePermissionActivityResult( + requestCode: Int, + onSlicePermissionResult: () -> Unit, + onUnhandledActivityResult: () -> Unit = {} +) { + if (requestCode == REQUEST_CODE_PERMISSIONS) { + onSlicePermissionResult.invoke() + } else { + onUnhandledActivityResult.invoke() + } +} + +private const val REQUEST_CODE_PERMISSIONS = 1234 diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c3903ed --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_action_apply.png b/app/src/main/res/drawable/ic_action_apply.png new file mode 100644 index 0000000..ee020b6 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_apply.png differ diff --git a/app/src/main/res/drawable/ic_action_clear.png b/app/src/main/res/drawable/ic_action_clear.png new file mode 100644 index 0000000..2a08e24 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_clear.png differ diff --git a/app/src/main/res/drawable/ic_action_name.png b/app/src/main/res/drawable/ic_action_name.png new file mode 100644 index 0000000..5ad26b8 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_name.png differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..5713f34 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/item_word_background.xml b/app/src/main/res/drawable/item_word_background.xml new file mode 100644 index 0000000..70186ea --- /dev/null +++ b/app/src/main/res/drawable/item_word_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_bottom_app_bar.xml b/app/src/main/res/layout/activity_bottom_app_bar.xml new file mode 100644 index 0000000..96b2811 --- /dev/null +++ b/app/src/main/res/layout/activity_bottom_app_bar.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chip.xml b/app/src/main/res/layout/activity_chip.xml new file mode 100644 index 0000000..b9e6135 --- /dev/null +++ b/app/src/main/res/layout/activity_chip.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..f336bb1 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_material.xml b/app/src/main/res/layout/activity_material.xml new file mode 100644 index 0000000..e2936f2 --- /dev/null +++ b/app/src/main/res/layout/activity_material.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_selection.xml b/app/src/main/res/layout/activity_selection.xml new file mode 100644 index 0000000..72bcb46 --- /dev/null +++ b/app/src/main/res/layout/activity_selection.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_slice_host.xml b/app/src/main/res/layout/activity_slice_host.xml new file mode 100644 index 0000000..610cc82 --- /dev/null +++ b/app/src/main/res/layout/activity_slice_host.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + +