diff --git a/mediacontroller/build.gradle b/mediacontroller/build.gradle index cad9097..a2d8abf 100644 --- a/mediacontroller/build.gradle +++ b/mediacontroller/build.gradle @@ -49,4 +49,5 @@ dependencies { implementation "androidx.leanback:leanback:$leanback_version" implementation 'com.android.support:design:28.0.0' implementation "com.google.android.material:material:$material_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" } diff --git a/mediacontroller/src/main/java/com/example/android/mediacontroller/MediaAppControllerActivity.java b/mediacontroller/src/main/java/com/example/android/mediacontroller/MediaAppControllerActivity.java index 0bed89e..b58d166 100644 --- a/mediacontroller/src/main/java/com/example/android/mediacontroller/MediaAppControllerActivity.java +++ b/mediacontroller/src/main/java/com/example/android/mediacontroller/MediaAppControllerActivity.java @@ -18,6 +18,7 @@ import static androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED; import static java.util.Arrays.asList; +import android.Manifest; import android.app.Activity; import android.app.PendingIntent; import android.content.Context; @@ -61,6 +62,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.drawable.DrawableCompat; @@ -73,6 +75,8 @@ import com.google.android.material.tabs.TabLayout; +import java.io.FileNotFoundException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -111,12 +115,16 @@ public class MediaAppControllerActivity extends AppCompatActivity { // Key name for Intent extras. private static final String APP_DETAILS_EXTRA = "com.example.android.mediacontroller.APP_DETAILS_EXTRA"; + private static final String DEFAULT_BROWSE_TREE_FILE_NAME = "_BrowseTreeContent.txt"; // Index values for spinner. private static final int SEARCH_INDEX = 0; private static final int MEDIA_ID_INDEX = 1; private static final int URI_INDEX = 2; + // Used for user storage permission request + private static final int CREATE_DOCUMENT_REQUEST_FOR_SNAPSHOT = 1; + private MediaAppDetails mMediaAppDetails; private MediaControllerCompat mController; private MediaBrowserCompat mBrowser; @@ -143,6 +151,8 @@ public class MediaAppControllerActivity extends AppCompatActivity { private ViewGroup mRatingViewGroup; + private MediaBrowseTreeSnapshot mMediaBrowseTreeSnapshot; + private final SparseArray mActionButtonMap = new SparseArray<>(); /** @@ -248,20 +258,20 @@ public Object instantiateItem(@NonNull ViewGroup container, int position) { browseTreeList.setHasFixedSize(true); browseTreeList.setAdapter(mBrowseMediaItemsAdapter); mBrowseMediaItemsAdapter.init(findViewById(R.id.media_browse_tree_top), - findViewById(R.id.media_browse_tree_up)); + findViewById(R.id.media_browse_tree_up), findViewById(R.id.media_browse_tree_save)); final RecyclerView browseTreeListExtraSuggested = findViewById(R.id.media_items_list_extra_suggested); browseTreeListExtraSuggested.setLayoutManager(new LinearLayoutManager(this)); browseTreeListExtraSuggested.setHasFixedSize(true); browseTreeListExtraSuggested.setAdapter(mBrowseMediaItemsExtraSuggestedAdapter); mBrowseMediaItemsExtraSuggestedAdapter.init(findViewById(R.id.media_browse_tree_top_extra_suggested), - findViewById(R.id.media_browse_tree_up_extra_suggested)); + findViewById(R.id.media_browse_tree_up_extra_suggested), findViewById(R.id.media_browse_tree_save)); final RecyclerView searchItemsList = findViewById(R.id.search_items_list); searchItemsList.setLayoutManager(new LinearLayoutManager(this)); searchItemsList.setHasFixedSize(true); searchItemsList.setAdapter(mSearchMediaItemsAdapter); - mSearchMediaItemsAdapter.init(null, null); + mSearchMediaItemsAdapter.init(null, null, null); findViewById(R.id.search_button).setOnClickListener(v -> { CharSequence queryText = ((TextView) findViewById(R.id.search_query)).getText(); @@ -271,6 +281,28 @@ public Object instantiateItem(@NonNull ViewGroup container, int position) { }); } + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CREATE_DOCUMENT_REQUEST_FOR_SNAPSHOT) { + if (resultCode == RESULT_OK && mMediaBrowseTreeSnapshot != null) { + Uri uri = data.getData(); + OutputStream outputStream = null; + try { + outputStream = getContentResolver().openOutputStream(uri); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + mMediaBrowseTreeSnapshot.takeBrowserSnapshot(outputStream); + Toast.makeText(this, "Output file location: " + uri.getPath(), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "File could not be saved.", Toast.LENGTH_SHORT).show(); + } + + + } + } + @Override protected void onDestroy() { if (mController != null) { @@ -1102,7 +1134,7 @@ private class BrowseMediaItemsAdapter extends RecyclerView.Adapter { private List mItems; - private Stack mNodes = new Stack<>(); + private final Stack mNodes = new Stack<>(); MediaBrowserCompat.SubscriptionCallback callback = new MediaBrowserCompat.SubscriptionCallback() { @@ -1206,7 +1238,7 @@ void updateItems(List items) { * Assigns click handlers to the buttons if provided for moving to the top of the tree or * for moving up one level in the tree. */ - void init(View topButtonView, View upButtonView) { + void init(View topButtonView, View upButtonView, View saveButtonView) { if (topButtonView != null) { topButtonView.setOnClickListener(v -> { if (mNodes.size() > 1) { @@ -1228,6 +1260,38 @@ void init(View topButtonView, View upButtonView) { } }); } + if (saveButtonView != null) { + saveButtonView.setOnClickListener(v -> { + takeMediaBrowseTreeSnapshot(); + }); + } + + } + + private void takeMediaBrowseTreeSnapshot(){ + if(mBrowser != null) { + if(mMediaBrowseTreeSnapshot == null) { + mMediaBrowseTreeSnapshot = new MediaBrowseTreeSnapshot( + MediaAppControllerActivity.this, mBrowser); + } + Intent saveTextFileIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + saveTextFileIntent.addCategory(Intent.CATEGORY_OPENABLE); + saveTextFileIntent.setType("text/plain"); + saveTextFileIntent.putExtra( + Intent.EXTRA_TITLE, DEFAULT_BROWSE_TREE_FILE_NAME); + MediaAppControllerActivity.this.startActivityForResult(saveTextFileIntent, + CREATE_DOCUMENT_REQUEST_FOR_SNAPSHOT); + + }else{ + Log.e(TAG, "Media browser is null"); + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(),"No media browser to snapshot", + Toast.LENGTH_SHORT).show(); + } + }); + } } protected void subscribe() { @@ -1284,7 +1348,8 @@ private class SearchMediaItemsAdapter extends BrowseMediaItemsAdapter { @Override protected void subscribe() { if (treeDepth() == 1) { - mBrowser.search(getCurrentNode(), null, new MediaBrowserCompat.SearchCallback() { + mBrowser.search(getCurrentNode(), null, + new MediaBrowserCompat.SearchCallback() { @Override public void onSearchResult(@NonNull String query, Bundle extras, @NonNull List items) { diff --git a/mediacontroller/src/main/java/com/example/android/mediacontroller/MediaBrowseTreeSnapshot.kt b/mediacontroller/src/main/java/com/example/android/mediacontroller/MediaBrowseTreeSnapshot.kt new file mode 100644 index 0000000..dff5cb8 --- /dev/null +++ b/mediacontroller/src/main/java/com/example/android/mediacontroller/MediaBrowseTreeSnapshot.kt @@ -0,0 +1,126 @@ +package com.example.android.mediacontroller + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.support.v4.media.MediaBrowserCompat +import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback +import android.util.Log +import android.widget.Toast +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import java.io.OutputStream +import java.io.PrintWriter +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + + +class MediaBrowseTreeSnapshot(private val context: Context, private val browser: MediaBrowserCompat):ViewModel() { + private val TAG = "MediaBrowseTreeSnapshot" + + + /** + * Loads the browsers top level children and runs a DFS on them printing out + * each media item's contentes as it is visited. + */ + fun takeBrowserSnapshot(outputStream: OutputStream) { + + viewModelScope.launch { + val mediaItems: MutableList = getChildNodes(browser.root) + if (mediaItems.isNotEmpty()) { + runDFSOnBrowseTree(mediaItems, outputStream) + for (item in mediaItems) { + Log.i(TAG, item.toString()) + } + } else { + notifyUser("No media items found, could not save tree.") + } + } + } + + private suspend fun getChildNodes(rootItemMid: String): MutableList = + suspendCoroutine { + val mediaItems: MutableList = ArrayList() + browser.subscribe(rootItemMid, object : SubscriptionCallback() { + override fun onChildrenLoaded(parentId: String, + children: List) { + // Notify the main thread that all of the children have loaded + mediaItems.addAll(children) + super.onChildrenLoaded(parentId, children) + it.resume(mediaItems) + } + }) + } + + /** + * Kicks off the browse tree depth first search by visiting all of the top level media + * item nodes. + */ + private suspend fun runDFSOnBrowseTree(mediaItems: MutableList, outputStream: OutputStream) { + val printWriter = PrintWriter(outputStream) + printWriter.println("Root:") + for (item in mediaItems) { + visitMediaItemNode(item, printWriter, 1) + } + printWriter.flush() + printWriter.close() + outputStream.close() + notifyUser("MediaItems saved to specified location.") + } + + /** + * Visits a media item node by printing out its contents and then visiting all of its children. + */ + private suspend fun visitMediaItemNode(mediaItem: MediaBrowserCompat.MediaItem?, printWriter: PrintWriter, depth: Int) { + if (mediaItem != null) { + printMediaItemDescription(printWriter, mediaItem, depth) + val mid = if (mediaItem.mediaId != null) mediaItem.mediaId!! else "" + + // If a media item is not a leaf continue DFS on it + if (mediaItem.isBrowsable && mid != "") { + + val mediaChildren: MutableList = getChildNodes(mid) + + // Run visit on all of the nodes children + for (mediaItemChild in mediaChildren) { + visitMediaItemNode(mediaItemChild, printWriter, depth + 1) + Log.i(TAG, "Visiting:" + mediaItemChild.toString()) + } + } + } + } + + /** + * Prints the contents of a media item using a print writer. + */ + private fun printMediaItemDescription(printWriter: PrintWriter, mediaItem: MediaBrowserCompat.MediaItem, depth: Int) { + val descriptionCompat = mediaItem.description + // Tab the media item to the respective depth + val tabStr = String(CharArray(depth)).replace("\u0000", + "\t") + val titleStr = if (descriptionCompat.title != null) descriptionCompat.title.toString() else "NAN" + val subTitleStr = if (descriptionCompat.subtitle != null) descriptionCompat.subtitle.toString() else "NAN" + val mIDStr = if (descriptionCompat.mediaId != null) descriptionCompat.mediaId else "NAN" + val uriStr = if (descriptionCompat.mediaUri != null) descriptionCompat.mediaUri.toString() else "NAN" + val desStr = if (descriptionCompat.description != null) descriptionCompat.description.toString() else "NAN" + val infoStr = String.format( + "%sTitle:%s,Subtitle:%s,MediaId:%s,URI:%s,Description:%s", + tabStr, titleStr, subTitleStr, mIDStr, uriStr, desStr) + printWriter.println(infoStr) + } + + /** + * Display formatted toast to user. + */ + private fun notifyUser(textToNotify: String) { + Handler(Looper.getMainLooper()).post { + val toast = Toast.makeText( + context, + textToNotify, + Toast.LENGTH_LONG) + toast.setMargin(50f, 50f) + toast.show() + } + } +} \ No newline at end of file diff --git a/mediacontroller/src/main/res/layout/media_browse_tree.xml b/mediacontroller/src/main/res/layout/media_browse_tree.xml index af8d11d..0e07591 100644 --- a/mediacontroller/src/main/res/layout/media_browse_tree.xml +++ b/mediacontroller/src/main/res/layout/media_browse_tree.xml @@ -44,6 +44,11 @@ android:layout_height="wrap_content" android:text="@string/media_browse_tree_up" /> +