From f8f2110ef95242b41ad2b55d94f7ca859caab771 Mon Sep 17 00:00:00 2001 From: Yacine Rezgui Date: Tue, 17 Aug 2021 09:02:18 +0100 Subject: [PATCH 01/22] Remove existing scoped storage sample --- ScopedStorage/.gitignore | 10 - ScopedStorage/README.md | 3 - ScopedStorage/app/.gitignore | 1 - ScopedStorage/app/build.gradle | 77 ---- ScopedStorage/app/proguard-rules.pro | 21 -- .../app/src/main/AndroidManifest.xml | 44 --- .../com/samples/storage/ActionListAdapter.kt | 67 ---- .../java/com/samples/storage/MainActivity.kt | 43 --- .../java/com/samples/storage/MainFragment.kt | 54 --- .../com/samples/storage/data/SampleFiles.kt | 55 --- .../storage/mediastore/AddDocumentFragment.kt | 134 ------- .../mediastore/AddDocumentViewModel.kt | 336 ------------------ .../storage/mediastore/AddMediaFragment.kt | 151 -------- .../storage/mediastore/AddMediaViewModel.kt | 189 ---------- .../storage/mediastore/CustomTakeVideo.kt | 42 --- .../storage/mediastore/DeleteMediaFragment.kt | 44 --- .../storage/mediastore/EditMediaFragment.kt | 44 --- .../storage/mediastore/MediaStoreFragment.kt | 59 --- .../com/samples/storage/saf/SafFragment.kt | 143 -------- .../storage/saf/SafFragmentViewModel.kt | 122 ------- .../res/drawable/ic_launcher_background.xml | 74 ---- .../res/drawable/ic_launcher_foreground.xml | 32 -- .../main/res/layout/fragment_add_document.xml | 108 ------ .../main/res/layout/fragment_add_media.xml | 93 ----- .../app/src/main/res/layout/fragment_demo.xml | 33 -- .../app/src/main/res/layout/fragment_list.xml | 27 -- .../app/src/main/res/layout/fragment_main.xml | 36 -- .../app/src/main/res/layout/fragment_saf.xml | 66 ---- .../app/src/main/res/layout/list_row_item.xml | 27 -- .../app/src/main/res/layout/main_activity.xml | 37 -- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 1926 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 3821 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1635 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2452 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 2583 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 5294 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 4578 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 8758 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 6199 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 12662 -> 0 bytes .../app/src/main/res/navigation/nav_graph.xml | 75 ---- .../app/src/main/res/values-night/themes.xml | 32 -- .../app/src/main/res/values/colors.xml | 26 -- .../app/src/main/res/values/strings.xml | 51 --- .../app/src/main/res/values/themes.xml | 32 -- ScopedStorage/build.gradle | 54 --- ScopedStorage/gradle.properties | 37 -- .../gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 22 -- ScopedStorage/gradlew | 172 --------- ScopedStorage/gradlew.bat | 84 ----- ScopedStorage/settings.gradle | 18 - ScopedStorage/spotless/copyright.kt | 15 - ScopedStorage/spotless/copyright.txt | 15 - 56 files changed, 2815 deletions(-) delete mode 100644 ScopedStorage/.gitignore delete mode 100644 ScopedStorage/README.md delete mode 100644 ScopedStorage/app/.gitignore delete mode 100644 ScopedStorage/app/build.gradle delete mode 100644 ScopedStorage/app/proguard-rules.pro delete mode 100644 ScopedStorage/app/src/main/AndroidManifest.xml delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/ActionListAdapter.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/MainActivity.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/MainFragment.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/data/SampleFiles.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddDocumentFragment.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddDocumentViewModel.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddMediaFragment.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddMediaViewModel.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/mediastore/CustomTakeVideo.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/mediastore/DeleteMediaFragment.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/mediastore/EditMediaFragment.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/mediastore/MediaStoreFragment.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/saf/SafFragment.kt delete mode 100644 ScopedStorage/app/src/main/java/com/samples/storage/saf/SafFragmentViewModel.kt delete mode 100644 ScopedStorage/app/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 ScopedStorage/app/src/main/res/drawable/ic_launcher_foreground.xml delete mode 100644 ScopedStorage/app/src/main/res/layout/fragment_add_document.xml delete mode 100644 ScopedStorage/app/src/main/res/layout/fragment_add_media.xml delete mode 100644 ScopedStorage/app/src/main/res/layout/fragment_demo.xml delete mode 100644 ScopedStorage/app/src/main/res/layout/fragment_list.xml delete mode 100644 ScopedStorage/app/src/main/res/layout/fragment_main.xml delete mode 100644 ScopedStorage/app/src/main/res/layout/fragment_saf.xml delete mode 100644 ScopedStorage/app/src/main/res/layout/list_row_item.xml delete mode 100644 ScopedStorage/app/src/main/res/layout/main_activity.xml delete mode 100644 ScopedStorage/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 ScopedStorage/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 ScopedStorage/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 ScopedStorage/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 ScopedStorage/app/src/main/res/navigation/nav_graph.xml delete mode 100644 ScopedStorage/app/src/main/res/values-night/themes.xml delete mode 100644 ScopedStorage/app/src/main/res/values/colors.xml delete mode 100644 ScopedStorage/app/src/main/res/values/strings.xml delete mode 100644 ScopedStorage/app/src/main/res/values/themes.xml delete mode 100644 ScopedStorage/build.gradle delete mode 100644 ScopedStorage/gradle.properties delete mode 100644 ScopedStorage/gradle/wrapper/gradle-wrapper.jar delete mode 100644 ScopedStorage/gradle/wrapper/gradle-wrapper.properties delete mode 100755 ScopedStorage/gradlew delete mode 100644 ScopedStorage/gradlew.bat delete mode 100644 ScopedStorage/settings.gradle delete mode 100644 ScopedStorage/spotless/copyright.kt delete mode 100644 ScopedStorage/spotless/copyright.txt diff --git a/ScopedStorage/.gitignore b/ScopedStorage/.gitignore deleted file mode 100644 index 878c2cd5..00000000 --- a/ScopedStorage/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*.iml -.gradle -/local.properties -.idea -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/ScopedStorage/README.md b/ScopedStorage/README.md deleted file mode 100644 index 4679cb10..00000000 --- a/ScopedStorage/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Storage APIs Demo Repository - -This repository contains a single Android Studio project, composed of modules illustrating how to use the Storage APIs on Android from API 19 (KitKat) until API 30 (Android 11) diff --git a/ScopedStorage/app/.gitignore b/ScopedStorage/app/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/ScopedStorage/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/ScopedStorage/app/build.gradle b/ScopedStorage/app/build.gradle deleted file mode 100644 index ecbadea0..00000000 --- a/ScopedStorage/app/build.gradle +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -plugins { - id 'com.android.application' - id 'kotlin-android' - id 'kotlin-parcelize' -} - -android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" - - defaultConfig { - applicationId "com.samples.storage" - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildFeatures { - viewBinding true - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.0' - implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.activity:activity-ktx:1.3.0-rc01' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.fragment:fragment-ktx:1.3.5' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' - implementation 'androidx.recyclerview:recyclerview:1.2.1' - implementation 'androidx.documentfile:documentfile:1.0.1' - implementation 'com.squareup.okhttp3:okhttp:4.9.1' - implementation 'com.github.bumptech.glide:glide:4.12.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' - - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} \ No newline at end of file diff --git a/ScopedStorage/app/proguard-rules.pro b/ScopedStorage/app/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/ScopedStorage/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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 \ No newline at end of file diff --git a/ScopedStorage/app/src/main/AndroidManifest.xml b/ScopedStorage/app/src/main/AndroidManifest.xml deleted file mode 100644 index 7e6e501b..00000000 --- a/ScopedStorage/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/ActionListAdapter.kt b/ScopedStorage/app/src/main/java/com/samples/storage/ActionListAdapter.kt deleted file mode 100644 index 8b7e65ac..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/ActionListAdapter.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.annotation.IdRes -import androidx.annotation.StringRes -import androidx.navigation.findNavController -import androidx.recyclerview.widget.RecyclerView - -data class Action(@StringRes val nameRes: Int, @IdRes val actionRes: Int) - -class ActionListAdapter(private val dataSet: Array) : - RecyclerView.Adapter() { - - /** - * Provide a reference to the type of views that you are using - * (custom ViewHolder). - */ - class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val textView: TextView = view.findViewById(R.id.textView) - - init { - // Define click listener for the ViewHolder's View. - } - } - - // Create new views (invoked by the layout manager) - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { - // Create a new view, which defines the UI of the list item - val view = LayoutInflater.from(viewGroup.context) - .inflate(R.layout.list_row_item, viewGroup, false) - - return ViewHolder(view) - } - - // Replace the contents of a view (invoked by the layout manager) - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - val context = viewHolder.textView.context - - // Get element from your dataset at this position and replace the - // contents of the view with that element - viewHolder.textView.text = context.getString(dataSet[position].nameRes) - viewHolder.textView.setOnClickListener { - it.findNavController().navigate(dataSet[position].actionRes) - } - } - - // Return the size of your dataset (invoked by the layout manager) - override fun getItemCount() = dataSet.size -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/MainActivity.kt b/ScopedStorage/app/src/main/java/com/samples/storage/MainActivity.kt deleted file mode 100644 index 29a8e030..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/MainActivity.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.findNavController -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.navigateUp -import androidx.navigation.ui.setupActionBarWithNavController - -class MainActivity : AppCompatActivity() { - private lateinit var appBarConfiguration: AppBarConfiguration - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.main_activity) - - val navController = findNavController(R.id.nav_host_fragment) - - appBarConfiguration = AppBarConfiguration(navController.graph) - setupActionBarWithNavController(navController, appBarConfiguration) - } - - override fun onSupportNavigateUp(): Boolean { - val navController = findNavController(R.id.nav_host_fragment) - return navController.navigateUp(appBarConfiguration) || - super.onSupportNavigateUp() - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/MainFragment.kt b/ScopedStorage/app/src/main/java/com/samples/storage/MainFragment.kt deleted file mode 100644 index 1b2269d0..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/MainFragment.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import com.samples.storage.databinding.FragmentListBinding - -private val apiList = arrayOf( - Action(R.string.demo_mediastore, R.id.action_mainFragment_to_mediaStoreFragment), - Action(R.string.demo_saf, R.id.action_mainFragment_to_safFragment) -) - -class MainFragment : Fragment() { - private var _binding: FragmentListBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentListBinding.inflate(inflater, container, false) - - binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) - binding.recyclerView.adapter = ActionListAdapter(apiList) - - binding.recyclerView - - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/data/SampleFiles.kt b/ScopedStorage/app/src/main/java/com/samples/storage/data/SampleFiles.kt deleted file mode 100644 index 82fc3e68..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/data/SampleFiles.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.data - -/** - * List of remote sample files to be used in the different samples - */ -object SampleFiles { - val images = listOf( - "https://storage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%20Balcony%20Toss/card.jpg", - "https://storage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%20Dance%20Search/card.jpg", - "https://storage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%20Extra%20Spicy/card.jpg", - "https://storage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%20Get%20Your%20Money's%20Worth/card.jpg" - ) - - val video = listOf( - "https://storage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%20Balcony%20Toss.mp4", - "https://storage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%20Dance%20Search.mp4", - "https://storage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%20Extra%20Spicy.mp4", - "https://storage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%20Get%20Your%20Money's%20Worth.mp4" - ) - - val media = images + video - - val texts = listOf( - "https://raw.githubusercontent.com/android/storage-samples/main/README.md", - "https://raw.githubusercontent.com/android/security-samples/main/README.md" - ) - - val documents = listOf( - "https://developer.android.com/images/jetpack/compose/compose-testing-cheatsheet.pdf", - "https://developer.android.com/images/training/dependency-injection/hilt-annotations.pdf", - "https://android.github.io/android-test/downloads/espresso-cheat-sheet-2.1.0.pdf" - ) - - val archives = listOf( - "https://github.com/android/storage-samples/archive/refs/heads/main.zip", - "https://github.com/android/security-samples/archive/refs/heads/main.zip" - ) - - val nonMedia = texts + documents + archives -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddDocumentFragment.kt b/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddDocumentFragment.kt deleted file mode 100644 index 673420eb..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddDocumentFragment.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.mediastore - -import android.Manifest -import android.os.Bundle -import android.text.format.DateUtils -import android.text.format.Formatter -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.samples.storage.R -import com.samples.storage.databinding.FragmentAddDocumentBinding -import kotlinx.coroutines.launch - -class AddDocumentFragment : Fragment() { - private var _binding: FragmentAddDocumentBinding? = null - private val binding get() = _binding!! - private val viewModel: AddDocumentViewModel by viewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentAddDocumentBinding.inflate(inflater, container, false) - - // Every time currentFileEntry is changed, we update the file details - viewModel.currentFileEntry.observe(viewLifecycleOwner) { fileDetails -> - if (fileDetails == null) { - binding.fileDetails.visibility = View.GONE - return@observe - } - - binding.filename.text = fileDetails.filename - binding.filePath.text = fileDetails.path - binding.fileSizeAndMimeType.text = getString( - R.string.mediastore_file_size_and_mimetype, - Formatter.formatShortFileSize(context, fileDetails.size), - fileDetails.mimeType - ) - binding.fileAddedAt.text = getString( - R.string.mediastore_file_added_at, - DateUtils.formatDateTime( - context, - fileDetails.addedAt, - DateUtils.FORMAT_SHOW_TIME or - DateUtils.FORMAT_SHOW_DATE or - DateUtils.FORMAT_SHOW_YEAR or - DateUtils.FORMAT_SHOW_WEEKDAY or - DateUtils.FORMAT_ABBREV_ALL - ) - ) - binding.fileDetails.visibility = View.VISIBLE - } - - // Every time isDownloading is changed, we toggle the download button - viewModel.isDownloading.observe(viewLifecycleOwner) { isDownloading -> - binding.downloadRandomFileFromInternet.isEnabled = !isDownloading - } - - binding.requestPermissionButton.setOnClickListener { - actionRequestPermission.launch( - arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - ) - } - - binding.downloadRandomFileFromInternet.setOnClickListener { - viewLifecycleOwner.lifecycleScope.launch { - - if (viewModel.canAddDocument) { - viewModel.addRandomFile() - } else { - showPermissionSection() - } - } - } - - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onResume() { - super.onResume() - handlePermissionSectionVisibility() - } - - private val actionRequestPermission = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - handlePermissionSectionVisibility() - } - - private fun handlePermissionSectionVisibility() { - if (viewModel.canAddDocument) { - hidePermissionSection() - } else { - showPermissionSection() - } - } - - private fun hidePermissionSection() { - binding.permissionSection.visibility = View.GONE - binding.actions.visibility = View.VISIBLE - } - - private fun showPermissionSection() { - binding.permissionSection.visibility = View.VISIBLE - binding.actions.visibility = View.GONE - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddDocumentViewModel.kt b/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddDocumentViewModel.kt deleted file mode 100644 index be677871..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddDocumentViewModel.kt +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.mediastore - -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.app.Application -import android.content.ContentValues -import android.content.Context -import android.content.pm.PackageManager -import android.media.MediaScannerConnection -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.os.Environment.DIRECTORY_DOWNLOADS -import android.os.Parcelable -import android.provider.MediaStore -import android.provider.MediaStore.Files.FileColumns -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import com.samples.storage.data.SampleFiles -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.ResponseBody -import java.io.File - -private const val TAG = "AddDocumentViewModel" - -class AddDocumentViewModel( - application: Application, - private val savedStateHandle: SavedStateHandle -) : AndroidViewModel(application) { - private val context: Context - get() = getApplication() - - /** - * Check ability to add document in the Download folder or not - */ - val canAddDocument: Boolean - get() = canAddDocumentPermission(context) - - /** - * Using lazy to instantiate the [OkHttpClient] only when accessing it, not when the viewmodel - * is created - */ - private val httpClient by lazy { OkHttpClient() } - - /** - * We keep the current [FileEntry] in the savedStateHandle to re-render it if there is a - * configuration change and we expose it as a [LiveData] to the UI - */ - private var _isDownloading: MutableLiveData = MutableLiveData(false) - val isDownloading: LiveData = _isDownloading - - /** - * We keep the current [FileEntry] in the savedStateHandle to re-render it if there is a - * configuration change and we expose it as a [LiveData] to the UI - */ - val currentFileEntry = savedStateHandle.getLiveData("current_file") - - /** - * Generate random filename when saving a new file - */ - private fun generateFilename(extension: String) = "${System.currentTimeMillis()}.$extension" - - /** - * Check if the app can writes on the shared storage - * - * On Android 10 (API 29), we can add files to the Downloads folder without having to request the - * [WRITE_EXTERNAL_STORAGE] permission, so we only check on pre-API 29 devices - */ - private fun canAddDocumentPermission(context: Context): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - true - } else { - ContextCompat.checkSelfPermission( - context, - WRITE_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED - } - } - - @Suppress("BlockingMethodInNonBlockingContext") - suspend fun addRandomFile() { - _isDownloading.postValue(true) - - val randomRemoteUrl = SampleFiles.nonMedia.random() - val extension = randomRemoteUrl.substring(randomRemoteUrl.lastIndexOf(".") + 1) - val filename = generateFilename(extension) - - withContext(Dispatchers.IO) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val newFileUri = addFileToDownloadsApi29(filename) - val outputStream = context.contentResolver.openOutputStream(newFileUri, "w") - ?: throw Exception("ContentResolver couldn't open $newFileUri outputStream") - - val responseBody = downloadFileFromInternet(randomRemoteUrl) - - if (responseBody == null) { - _isDownloading.postValue(false) - return@withContext - } - - // .use is an extension function that closes the output stream where we're - // saving the file content once its lambda is finished being executed - responseBody.use { - outputStream.use { - responseBody.byteStream().copyTo(it) - } - } - - Log.d(TAG, "File downloaded ($newFileUri)") - - val path = getMediaStoreEntryPathApi29(newFileUri) - ?: throw Exception("ContentResolver couldn't find $newFileUri") - - // We scan the newly added file to make sure MediaStore.Downloads is always up - // to date - scanFilePath(path, responseBody.contentType().toString()) { uri -> - Log.d(TAG, "MediaStore updated ($path, $uri)") - - viewModelScope.launch { - val fileDetails = getFileDetails(uri) - Log.d(TAG, "New file: $fileDetails") - - savedStateHandle["current_file"] = fileDetails - _isDownloading.postValue(false) - } - } - } else { - val file = addFileToDownloadsApi21(filename) - val outputStream = file.outputStream() - - val responseBody = downloadFileFromInternet(randomRemoteUrl) - - if (responseBody == null) { - _isDownloading.postValue(false) - return@withContext - } - - // .use is an extension function that closes the output stream where we're - // saving the file content once its lambda is finished being executed - responseBody.use { - outputStream.use { - responseBody.byteStream().copyTo(it) - } - } - - Log.d(TAG, "File downloaded (${file.absolutePath})") - - // We scan the newly added file to make sure MediaStore.Files is always up to - // date - scanFilePath(file.path, responseBody.contentType().toString()) { uri -> - Log.d(TAG, "MediaStore updated ($file.path, $uri)") - - viewModelScope.launch { - val fileDetails = getFileDetails(uri) - Log.d(TAG, "New file: $fileDetails") - - savedStateHandle["current_file"] = fileDetails - _isDownloading.postValue(false) - } - } - } - } catch (e: Exception) { - Log.e(TAG, e.toString()) - _isDownloading.postValue(false) - } - } - } - - /** - * Downloads a random file from internet and saves its content to the specified outputStream - */ - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun downloadFileFromInternet(url: String): ResponseBody? { - // We use OkHttp to create HTTP request - val request = Request.Builder().url(url).build() - - return withContext(Dispatchers.IO) { - val response = httpClient.newCall(request).execute() - return@withContext response.body - } - } - - /** - * Create a file inside the Download folder using java.io API - */ - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun addFileToDownloadsApi21(filename: String): File { - val downloadsFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) - - // Get path of the destination where the file will be saved - val newNonMediaFile = File(downloadsFolder, filename) - - return withContext(Dispatchers.IO) { - // Create new file if it does not exist, throw exception otherwise - if (!newNonMediaFile.createNewFile()) { - throw Exception("File ${newNonMediaFile.name} already exists") - } - - return@withContext newNonMediaFile - } - } - - /** - * Create a file inside the Download folder using MediaStore API - */ - @Suppress("BlockingMethodInNonBlockingContext") - @RequiresApi(Build.VERSION_CODES.Q) - private suspend fun addFileToDownloadsApi29(filename: String): Uri { - val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - - return withContext(Dispatchers.IO) { - val newFile = ContentValues().apply { - put(MediaStore.Downloads.DISPLAY_NAME, filename) - } - - // This method will perform a binder transaction which is better to execute off the main - // thread - return@withContext context.contentResolver.insert(collection, newFile) - ?: throw Exception("MediaStore Uri couldn't be created") - } - } - - /** - * When adding a file (using java.io or ContentResolver APIs), MediaStore might not be aware of - * the new entry or doesn't have an updated version of it. That's why some entries have 0 bytes - * size, even though the file is definitely not empty. MediaStore will eventually scan the file - * but it's better to do it ourselves to have a fresher state whenever we can - */ - private suspend fun scanFilePath(path: String, mimeType: String, callback: (uri: Uri) -> Unit) { - withContext(Dispatchers.IO) { - MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, uri -> - callback(uri) - } - } - } - - /** - * Get a path for a MediaStore entry as it's needed when calling MediaScanner - */ - private suspend fun getMediaStoreEntryPathApi29(uri: Uri): String? { - return withContext(Dispatchers.IO) { - val cursor = context.contentResolver.query( - uri, - arrayOf(FileColumns.DATA), - null, - null, - null - ) ?: return@withContext null - - cursor.use { - if (!cursor.moveToFirst()) { - return@withContext null - } - - return@withContext cursor.getString(cursor.getColumnIndexOrThrow(FileColumns.DATA)) - } - } - } - - /** - * Get file details using the MediaStore API - */ - private suspend fun getFileDetails(uri: Uri): FileEntry? { - return withContext(Dispatchers.IO) { - val cursor = context.contentResolver.query( - uri, - arrayOf( - FileColumns.DISPLAY_NAME, - FileColumns.SIZE, - FileColumns.MIME_TYPE, - FileColumns.DATE_ADDED, - FileColumns.DATA - ), - null, - null, - null - ) ?: return@withContext null - - cursor.use { - if (!cursor.moveToFirst()) { - return@withContext null - } - - val displayNameColumn = cursor.getColumnIndexOrThrow(FileColumns.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndexOrThrow(FileColumns.SIZE) - val mimeTypeColumn = cursor.getColumnIndexOrThrow(FileColumns.MIME_TYPE) - val dateAddedColumn = cursor.getColumnIndexOrThrow(FileColumns.DATE_ADDED) - val dataColumn = cursor.getColumnIndexOrThrow(FileColumns.DATA) - - return@withContext FileEntry( - filename = cursor.getString(displayNameColumn), - size = cursor.getLong(sizeColumn), - mimeType = cursor.getString(mimeTypeColumn), - // FileColumns.DATE_ADDED is in seconds, not milliseconds - addedAt = cursor.getLong(dateAddedColumn) * 1000, - path = cursor.getString(dataColumn), - ) - } - } - } -} - -@Parcelize -data class FileEntry( - val filename: String, - val size: Long, - val mimeType: String, - val addedAt: Long, - val path: String -) : Parcelable diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddMediaFragment.kt b/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddMediaFragment.kt deleted file mode 100644 index 2c761e1b..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddMediaFragment.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.mediastore - -import android.Manifest.permission.READ_EXTERNAL_STORAGE -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions -import androidx.activity.result.contract.ActivityResultContracts.TakePicture -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.bumptech.glide.Glide -import com.samples.storage.databinding.FragmentAddMediaBinding -import kotlinx.coroutines.launch - -class AddMediaFragment : Fragment() { - private var _binding: FragmentAddMediaBinding? = null - private val binding get() = _binding!! - private val viewModel: AddMediaViewModel by viewModels() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = FragmentAddMediaBinding.inflate(inflater, container, false) - - // Every time currentMediaUri is changed, we update the ImageView - viewModel.currentMediaUri.observe(viewLifecycleOwner) { uri -> - Glide.with(this).load(uri).into(binding.mediaThumbnail) - } - - binding.requestPermissionButton.setOnClickListener { - actionRequestPermission.launch(arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)) - } - - binding.takePictureButton.setOnClickListener { - viewLifecycleOwner.lifecycleScope.launch { - - if (viewModel.canWriteInMediaStore) { - viewModel.createPhotoUri(Source.CAMERA)?.let { uri -> - viewModel.saveTemporarilyPhotoUri(uri) - actionTakePicture.launch(uri) - } - } else { - showPermissionSection() - } - } - } - - binding.takeVideoButton.setOnClickListener { - viewLifecycleOwner.lifecycleScope.launch { - - if (viewModel.canWriteInMediaStore) { - viewModel.createVideoUri(Source.CAMERA)?.let { uri -> - actionTakeVideo.launch(uri) - } - } else { - showPermissionSection() - } - } - } - - binding.downloadImageFromInternetButton.setOnClickListener { - - if (viewModel.canWriteInMediaStore) { - binding.downloadImageFromInternetButton.isEnabled = false - viewModel.saveRandomImageFromInternet { - // We re-enable the button once the download is done - // Keep in mind the logic is basic as it doesn't handle errors - binding.downloadImageFromInternetButton.isEnabled = true - } - } else { - showPermissionSection() - } - } - - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onResume() { - super.onResume() - - handlePermissionSectionVisibility() - } - - private fun handlePermissionSectionVisibility() { - if (viewModel.canWriteInMediaStore) { - hidePermissionSection() - } else { - showPermissionSection() - } - } - - private fun hidePermissionSection() { - binding.permissionSection.visibility = View.GONE - binding.actions.visibility = View.VISIBLE - } - - private fun showPermissionSection() { - binding.permissionSection.visibility = View.VISIBLE - binding.actions.visibility = View.GONE - } - - private val actionRequestPermission = registerForActivityResult(RequestMultiplePermissions()) { - handlePermissionSectionVisibility() - } - - private val actionTakePicture = registerForActivityResult(TakePicture()) { success -> - if (!success) { - Log.d(tag, "Image taken FAIL") - return@registerForActivityResult - } - - Log.d(tag, "Image taken SUCCESS") - - viewModel.temporaryPhotoUri?.let { - viewModel.loadCameraMedia(it) - viewModel.saveTemporarilyPhotoUri(null) - } - } - - private val actionTakeVideo = registerForActivityResult(CustomTakeVideo()) { uri -> - if (uri == null) { - Log.d(tag, "Video taken FAIL") - return@registerForActivityResult - } - - Log.d(tag, "Video taken SUCCESS") - viewModel.loadCameraMedia(uri) - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddMediaViewModel.kt b/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddMediaViewModel.kt deleted file mode 100644 index 4e76fa3e..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/AddMediaViewModel.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.mediastore - -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.app.Application -import android.content.ContentValues -import android.content.Context -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.provider.MediaStore -import androidx.core.content.ContextCompat.checkSelfPermission -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request - -/** - * URL returning random picture provided by Unsplash. Read more here: https://source.unsplash.com - */ -private const val RANDOM_IMAGE_URL = "https://source.unsplash.com/random/500x500" - -class AddMediaViewModel(application: Application, private val savedStateHandle: SavedStateHandle) : AndroidViewModel(application) { - - private val context: Context - get() = getApplication() - - val canWriteInMediaStore: Boolean - get() = checkMediaStorePermission(context) - - /** - * Using lazy to instantiate the [OkHttpClient] only when accessing it, not when the viewmodel - * is created - */ - private val httpClient by lazy { OkHttpClient() } - - /** - * We keep the current media [Uri] in the savedStateHandle to re-render it if there is a - * configuration change and we expose it as a [LiveData] to the UI - */ - val currentMediaUri: LiveData = savedStateHandle.getLiveData("currentMediaUri") - - /** - * TakePicture activityResult action isn't returning the [Uri] once the image has been taken, so - * we need to save the temporarily created URI in [savedStateHandle] until we handle its result - */ - fun saveTemporarilyPhotoUri(uri: Uri?) { - savedStateHandle["temporaryPhotoUri"] = uri - } - - val temporaryPhotoUri: Uri? - get() = savedStateHandle.get("temporaryPhotoUri") - - /** - * [loadCameraMedia] is called when TakePicture or TakeVideo intent is returning a - * successful result, that we set to the currentMediaUri property, which will - * trigger to load the thumbnail in the UI - */ - fun loadCameraMedia(uri: Uri) { - savedStateHandle["currentMediaUri"] = uri - } - - /** - * We create a [Uri] where the image will be stored - */ - suspend fun createPhotoUri(source: Source): Uri? { - val imageCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - } else { - MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } - - return withContext(Dispatchers.IO) { - val newImage = ContentValues().apply { - put(MediaStore.Images.Media.DISPLAY_NAME, generateFilename(source, "jpg")) - } - - // This method will perform a binder transaction which is better to execute off the main - // thread - return@withContext context.contentResolver.insert(imageCollection, newImage) - } - } - - /** - * We create a [Uri] where the camera will store the video - */ - suspend fun createVideoUri(source: Source): Uri? { - val videoCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - } else { - MediaStore.Video.Media.EXTERNAL_CONTENT_URI - } - - return withContext(Dispatchers.IO) { - val newVideo = ContentValues().apply { - put(MediaStore.Video.Media.DISPLAY_NAME, generateFilename(source, "mp4")) - } - - // This method will perform a binder transaction which is better to execute off the main - // thread - return@withContext context.contentResolver.insert(videoCollection, newVideo) - } - } - - /** - * [saveRandomImageFromInternet] downloads a random image from unsplash.com and saves its - * content - */ - fun saveRandomImageFromInternet(callback: () -> Unit) { - viewModelScope.launch { - val imageUri = createPhotoUri(Source.INTERNET) - // We use OkHttp to create HTTP request - val request = Request.Builder().url(RANDOM_IMAGE_URL).build() - - withContext(Dispatchers.IO) { - - imageUri?.let { destinationUri -> - val response = httpClient.newCall(request).execute() - - // .use is an extension function that closes the output stream where we're - // saving the image content once its lambda is finished being executed - response.body?.use { responseBody -> - context.contentResolver.openOutputStream(destinationUri, "w")?.use { - responseBody.byteStream().copyTo(it) - - /** - * We can't set savedStateHandle within a background thread, so we do it - * within the [Dispatchers.Main], which execute its coroutines on the - * main thread - */ - withContext(Dispatchers.Main) { - savedStateHandle["currentMediaUri"] = destinationUri - callback() - } - } - } - } - } - } - } -} - -/** - * Check if the app can writes on the shared storage - * - * On Android 10 (API 29), we can add media to MediaStore without having to request the - * [WRITE_EXTERNAL_STORAGE] permission, so we only check on pre-API 29 devices - */ -private fun checkMediaStorePermission(context: Context): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - true - } else { - checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED - } -} - -enum class Source { - CAMERA, INTERNET -} - -private fun generateFilename(source: Source, extension: String): String { - return when (source) { - Source.CAMERA -> { - "camera-${System.currentTimeMillis()}.$extension" - } - Source.INTERNET -> { - "internet-${System.currentTimeMillis()}.$extension" - } - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/CustomTakeVideo.kt b/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/CustomTakeVideo.kt deleted file mode 100644 index 51139947..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/CustomTakeVideo.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.mediastore - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.provider.MediaStore -import androidx.activity.result.contract.ActivityResultContract - -class CustomTakeVideo : ActivityResultContract() { - override fun createIntent(context: Context, input: Uri): Intent { - return Intent(MediaStore.ACTION_VIDEO_CAPTURE) - .putExtra(MediaStore.EXTRA_OUTPUT, input) - } - - override fun getSynchronousResult(context: Context, input: Uri): SynchronousResult? { - return null - } - - override fun parseResult(resultCode: Int, intent: Intent?): Uri? { - return if (intent == null || resultCode != Activity.RESULT_OK) { - null - } else { - intent.data - } - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/DeleteMediaFragment.kt b/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/DeleteMediaFragment.kt deleted file mode 100644 index c9dc691b..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/DeleteMediaFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.mediastore - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.samples.storage.databinding.FragmentDemoBinding - -// TODO(yrezgui): Finish this demo -class DeleteMediaFragment : Fragment() { - private var _binding: FragmentDemoBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentDemoBinding.inflate(inflater, container, false) - val view = binding.root - return view - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/EditMediaFragment.kt b/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/EditMediaFragment.kt deleted file mode 100644 index 9a15af84..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/EditMediaFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.mediastore - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.samples.storage.databinding.FragmentDemoBinding - -// TODO(yrezgui): Finish this demo -class EditMediaFragment : Fragment() { - private var _binding: FragmentDemoBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentDemoBinding.inflate(inflater, container, false) - val view = binding.root - return view - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/MediaStoreFragment.kt b/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/MediaStoreFragment.kt deleted file mode 100644 index 4705653e..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/mediastore/MediaStoreFragment.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.mediastore - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import com.samples.storage.Action -import com.samples.storage.ActionListAdapter -import com.samples.storage.R -import com.samples.storage.databinding.FragmentListBinding - -private val demoList = arrayOf( - Action(R.string.mediastore_add, R.id.action_mediaStoreFragment_to_addMediaFragment), - Action(R.string.mediastore_edit, R.id.action_mediaStoreFragment_to_editMediaFragment), - Action(R.string.mediastore_delete, R.id.action_mediaStoreFragment_to_deleteMediaFragment), - Action(R.string.mediastore_downloads, R.id.action_mediaStoreFragment_to_addDocumentFragment), -) - -class MediaStoreFragment : Fragment() { - private var _binding: FragmentListBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentListBinding.inflate(inflater, container, false) - - binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) - binding.recyclerView.adapter = ActionListAdapter(demoList) - - binding.recyclerView - - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/saf/SafFragment.kt b/ScopedStorage/app/src/main/java/com/samples/storage/saf/SafFragment.kt deleted file mode 100644 index a5138524..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/saf/SafFragment.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.saf - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts.CreateDocument -import androidx.activity.result.contract.ActivityResultContracts.OpenDocument -import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree -import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.samples.storage.R -import com.samples.storage.databinding.FragmentSafBinding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -private const val DEFAULT_FILE_NAME = "SAF Demo File.txt" - -/** - * Fragment that demonstrates the most common ways to work with documents via the - * Storage Access Framework (SAF). - */ -class SafFragment : Fragment() { - private var _binding: FragmentSafBinding? = null - private val binding get() = _binding!! - - private val viewModel: SafFragmentViewModel by viewModels() - - private val actionCreateDocument = registerForActivityResult(CreateDocument()) { uri -> - // If the user returns to this fragment without creating a file, uri will be null - // In this case, we return void - val documentUri = uri ?: return@registerForActivityResult - - // If we can't instantiate a `DocumentFile`, it probably means the file has been removed - // or became unavailable (if the SD card has been removed). - // In this case, we return void - val documentFile = DocumentFile.fromSingleUri(requireContext(), documentUri) - ?: return@registerForActivityResult - - // We launch a coroutine within the lifecycle of the viewmodel. The coroutine will be - // automatically cancelled if the viewmodel is cleared - viewLifecycleOwner.lifecycleScope.launch { - @Suppress("BlockingMethodInNonBlockingContext") - val documentStream = withContext(Dispatchers.IO) { - requireContext().contentResolver.openOutputStream(documentUri) - } ?: return@launch - - val text = viewModel.createDocumentExample(documentStream) - binding.output.text = - getString(R.string.saf_create_file_output, documentFile.name, text) - } - - Log.d("SafFragment", "Created: ${documentFile.name}, type ${documentFile.type}") - } - - private val actionOpenDocument = registerForActivityResult(OpenDocument()) { uri -> - // If the user returns to this fragment without selecting a file, uri will be null - // In this case, we return void - val documentUri = uri ?: return@registerForActivityResult - - // If we can't instantiate a `DocumentFile`, it probably means the file has been removed - // or became unavailable (if the SD card has been removed). - // In this case, we return void - val documentFile = DocumentFile.fromSingleUri(requireContext(), documentUri) - ?: return@registerForActivityResult - - viewLifecycleOwner.lifecycleScope.launch { - @Suppress("BlockingMethodInNonBlockingContext") - val documentStream = withContext(Dispatchers.IO) { - requireContext().contentResolver.openInputStream(documentUri) - } ?: return@launch - - val text = viewModel.openDocumentExample(documentStream) - binding.output.text = getString(R.string.saf_open_file_output, documentFile.name, text) - } - } - - private val actionOpenDocumentTree = registerForActivityResult(OpenDocumentTree()) { uri -> - val documentUri = uri ?: return@registerForActivityResult - val context = requireContext().applicationContext - val parentFolder = DocumentFile.fromTreeUri(context, documentUri) - ?: return@registerForActivityResult - - viewLifecycleOwner.lifecycleScope.launch { - val text = viewModel.listFiles(parentFolder) - .sortedBy { it.first } - .joinToString { it.first } - binding.output.text = getString(R.string.saf_folder_output, text) - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSafBinding.inflate(inflater, container, false) - - binding.createFile.setOnClickListener { - // We ask the user to create a file with a preferred default filename, which can be - // overwritten by the user - actionCreateDocument.launch(DEFAULT_FILE_NAME) - } - - binding.openFile.setOnClickListener { - // We ask the user to select any file. If we want to select a specific one, we would do - // this: `actionOpenDocument.launch(arrayOf("image/png", "image/gif"))` - actionOpenDocument.launch(arrayOf("*/*")) - } - - binding.openFolder.setOnClickListener { - // We ask the user to select a folder. We can specify a preferred folder to be opened - // if we have its URI and the device is running on API 26+ - actionOpenDocumentTree.launch(null) - } - - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/ScopedStorage/app/src/main/java/com/samples/storage/saf/SafFragmentViewModel.kt b/ScopedStorage/app/src/main/java/com/samples/storage/saf/SafFragmentViewModel.kt deleted file mode 100644 index 7bde32bf..00000000 --- a/ScopedStorage/app/src/main/java/com/samples/storage/saf/SafFragmentViewModel.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.samples.storage.saf - -import android.Manifest.permission.MANAGE_EXTERNAL_STORAGE -import android.content.ContentResolver -import android.content.Intent -import android.net.Uri -import android.provider.DocumentsContract -import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.InputStream -import java.io.OutputStream -import java.nio.charset.StandardCharsets -import java.security.MessageDigest -import java.util.Locale -import kotlin.random.Random - -/** Number of bytes to read at a time from an open stream */ -private const val FILE_BUFFER_SIZE_BYTES = 1024 - -/** - * ViewModel contains various examples for how to work with the contents of documents - * opened with the Storage Access Framework. - */ -class SafFragmentViewModel : ViewModel() { - - /** - * It's easiest to work with documents selected with the [Intent.ACTION_CREATE_DOCUMENT] action - * by simply opening an [OutputStream]. In this example we're generating some random text - * based on the words found in "Lorem Ipsum". - */ - suspend fun createDocumentExample(outputStream: OutputStream): String { - - @Suppress("BlockingMethodInNonBlockingContext") - return withContext(Dispatchers.IO) { - val lines = mutableListOf() - - for (lineNumber in 1..Random.nextInt(1, 5)) { - val line = "hello world ".repeat(Random.nextInt(1, 5)) - lines += line.capitalize(Locale.US) - } - - val contents = lines.joinToString(separator = System.lineSeparator()) - - outputStream.bufferedWriter(StandardCharsets.UTF_8).use { writer -> - writer.write(contents) - } - contents - } - } - - /** - * Similar to [Intent.ACTION_CREATE_DOCUMENT], it's easiest to work with documents selected - * with the [Intent.ACTION_OPEN_DOCUMENT] action by simply opening an [InputStream] or - * [OutputStream], depending on the need. In this example, since we don't want to disturb the - * contents of the file, we're just going to use an [InputStream] to generate a hash of - * the file's contents. - * - * Since hashing the contents of a large file may take some time, this is done in a - * suspend function with the [Dispatchers.IO] coroutine context. - */ - suspend fun openDocumentExample(inputStream: InputStream): String { - @Suppress("BlockingMethodInNonBlockingContext") - return withContext(Dispatchers.IO) { - inputStream.use { stream -> - val messageDigest = MessageDigest.getInstance("SHA-256") - - val buffer = ByteArray(FILE_BUFFER_SIZE_BYTES) - var bytesRead = stream.read(buffer) - while (bytesRead > 0) { - messageDigest.update(buffer, 0, bytesRead) - bytesRead = stream.read(buffer) - } - val hashResult = messageDigest.digest() - hashResult.joinToString(separator = ":") { "%02x".format(it) } - } - } - } - - /** - * Simple example of using [DocumentFile] to get all the documents in a folder (by using - * [Intent.ACTION_OPEN_DOCUMENT_TREE]). - * It's possible to use [DocumentsContract] and [ContentResolver] directly, but using - * [DocumentFile] allows us to access an easier to use API. - * - * While it's _possible_ to search across multiple directories and recursively work with files - * via SAF, there can be significant performance penalties to this type of usage. If your - * use case requires this, consider looking into the permission [MANAGE_EXTERNAL_STORAGE]. - * - * Accessing any field in the [DocumentFile] object, aside from [DocumentFile.getUri], - * ultimately performs a lookup with the system's [ContentResolver], and should thus be - * performed off the main thread, which is why we're doing this transformation from - * [DocumentFile] to file name and [Uri] in a coroutine. - */ - suspend fun listFiles(folder: DocumentFile): List> { - return withContext(Dispatchers.IO) { - if (folder.isDirectory) { - folder.listFiles().mapNotNull { file -> - if (file.name != null) Pair(file.name!!, file.uri) else null - } - } else { - emptyList() - } - } - } -} diff --git a/ScopedStorage/app/src/main/res/drawable/ic_launcher_background.xml b/ScopedStorage/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index ca3826a4..00000000 --- a/ScopedStorage/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ScopedStorage/app/src/main/res/drawable/ic_launcher_foreground.xml b/ScopedStorage/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 4535491a..00000000 --- a/ScopedStorage/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - diff --git a/ScopedStorage/app/src/main/res/layout/fragment_add_document.xml b/ScopedStorage/app/src/main/res/layout/fragment_add_document.xml deleted file mode 100644 index 86a38ef4..00000000 --- a/ScopedStorage/app/src/main/res/layout/fragment_add_document.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - -