Skip to content

Commit 528a692

Browse files
author
Leo Neat
authored
Merge pull request #28 from Leo-Neat/master
[Infra] Adding a button to snapshot the current Media Browse Tree
2 parents cce593b + 0e80bcf commit 528a692

File tree

5 files changed

+204
-6
lines changed

5 files changed

+204
-6
lines changed

mediacontroller/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ dependencies {
4949
implementation "androidx.leanback:leanback:$leanback_version"
5050
implementation 'com.android.support:design:28.0.0'
5151
implementation "com.google.android.material:material:$material_version"
52+
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
5253
}

mediacontroller/src/main/java/com/example/android/mediacontroller/MediaAppControllerActivity.java

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED;
1919
import static java.util.Arrays.asList;
2020

21+
import android.Manifest;
2122
import android.app.Activity;
2223
import android.app.PendingIntent;
2324
import android.content.Context;
@@ -61,6 +62,7 @@
6162
import androidx.appcompat.app.ActionBar;
6263
import androidx.appcompat.app.AppCompatActivity;
6364
import androidx.appcompat.widget.Toolbar;
65+
import androidx.core.app.ActivityCompat;
6466
import androidx.core.content.ContextCompat;
6567
import androidx.core.content.res.ResourcesCompat;
6668
import androidx.core.graphics.drawable.DrawableCompat;
@@ -73,6 +75,8 @@
7375

7476
import com.google.android.material.tabs.TabLayout;
7577

78+
import java.io.FileNotFoundException;
79+
import java.io.OutputStream;
7680
import java.util.ArrayList;
7781
import java.util.Collections;
7882
import java.util.Comparator;
@@ -111,12 +115,16 @@ public class MediaAppControllerActivity extends AppCompatActivity {
111115
// Key name for Intent extras.
112116
private static final String APP_DETAILS_EXTRA =
113117
"com.example.android.mediacontroller.APP_DETAILS_EXTRA";
118+
private static final String DEFAULT_BROWSE_TREE_FILE_NAME = "_BrowseTreeContent.txt";
114119

115120
// Index values for spinner.
116121
private static final int SEARCH_INDEX = 0;
117122
private static final int MEDIA_ID_INDEX = 1;
118123
private static final int URI_INDEX = 2;
119124

125+
// Used for user storage permission request
126+
private static final int CREATE_DOCUMENT_REQUEST_FOR_SNAPSHOT = 1;
127+
120128
private MediaAppDetails mMediaAppDetails;
121129
private MediaControllerCompat mController;
122130
private MediaBrowserCompat mBrowser;
@@ -143,6 +151,8 @@ public class MediaAppControllerActivity extends AppCompatActivity {
143151

144152
private ViewGroup mRatingViewGroup;
145153

154+
private MediaBrowseTreeSnapshot mMediaBrowseTreeSnapshot;
155+
146156
private final SparseArray<ImageButton> mActionButtonMap = new SparseArray<>();
147157

148158
/**
@@ -248,20 +258,20 @@ public Object instantiateItem(@NonNull ViewGroup container, int position) {
248258
browseTreeList.setHasFixedSize(true);
249259
browseTreeList.setAdapter(mBrowseMediaItemsAdapter);
250260
mBrowseMediaItemsAdapter.init(findViewById(R.id.media_browse_tree_top),
251-
findViewById(R.id.media_browse_tree_up));
261+
findViewById(R.id.media_browse_tree_up), findViewById(R.id.media_browse_tree_save));
252262

253263
final RecyclerView browseTreeListExtraSuggested = findViewById(R.id.media_items_list_extra_suggested);
254264
browseTreeListExtraSuggested.setLayoutManager(new LinearLayoutManager(this));
255265
browseTreeListExtraSuggested.setHasFixedSize(true);
256266
browseTreeListExtraSuggested.setAdapter(mBrowseMediaItemsExtraSuggestedAdapter);
257267
mBrowseMediaItemsExtraSuggestedAdapter.init(findViewById(R.id.media_browse_tree_top_extra_suggested),
258-
findViewById(R.id.media_browse_tree_up_extra_suggested));
268+
findViewById(R.id.media_browse_tree_up_extra_suggested), findViewById(R.id.media_browse_tree_save));
259269

260270
final RecyclerView searchItemsList = findViewById(R.id.search_items_list);
261271
searchItemsList.setLayoutManager(new LinearLayoutManager(this));
262272
searchItemsList.setHasFixedSize(true);
263273
searchItemsList.setAdapter(mSearchMediaItemsAdapter);
264-
mSearchMediaItemsAdapter.init(null, null);
274+
mSearchMediaItemsAdapter.init(null, null, null);
265275

266276
findViewById(R.id.search_button).setOnClickListener(v -> {
267277
CharSequence queryText = ((TextView) findViewById(R.id.search_query)).getText();
@@ -271,6 +281,28 @@ public Object instantiateItem(@NonNull ViewGroup container, int position) {
271281
});
272282
}
273283

284+
@Override
285+
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
286+
super.onActivityResult(requestCode, resultCode, data);
287+
if (requestCode == CREATE_DOCUMENT_REQUEST_FOR_SNAPSHOT) {
288+
if (resultCode == RESULT_OK && mMediaBrowseTreeSnapshot != null) {
289+
Uri uri = data.getData();
290+
OutputStream outputStream = null;
291+
try {
292+
outputStream = getContentResolver().openOutputStream(uri);
293+
} catch (FileNotFoundException e) {
294+
e.printStackTrace();
295+
}
296+
mMediaBrowseTreeSnapshot.takeBrowserSnapshot(outputStream);
297+
Toast.makeText(this, "Output file location: " + uri.getPath(), Toast.LENGTH_SHORT).show();
298+
} else {
299+
Toast.makeText(this, "File could not be saved.", Toast.LENGTH_SHORT).show();
300+
}
301+
302+
303+
}
304+
}
305+
274306
@Override
275307
protected void onDestroy() {
276308
if (mController != null) {
@@ -1102,7 +1134,7 @@ private class BrowseMediaItemsAdapter extends
11021134
RecyclerView.Adapter<BrowseMediaItemsAdapter.ViewHolder> {
11031135

11041136
private List<MediaBrowserCompat.MediaItem> mItems;
1105-
private Stack<String> mNodes = new Stack<>();
1137+
private final Stack<String> mNodes = new Stack<>();
11061138

11071139
MediaBrowserCompat.SubscriptionCallback callback =
11081140
new MediaBrowserCompat.SubscriptionCallback() {
@@ -1206,7 +1238,7 @@ void updateItems(List items) {
12061238
* Assigns click handlers to the buttons if provided for moving to the top of the tree or
12071239
* for moving up one level in the tree.
12081240
*/
1209-
void init(View topButtonView, View upButtonView) {
1241+
void init(View topButtonView, View upButtonView, View saveButtonView) {
12101242
if (topButtonView != null) {
12111243
topButtonView.setOnClickListener(v -> {
12121244
if (mNodes.size() > 1) {
@@ -1228,6 +1260,38 @@ void init(View topButtonView, View upButtonView) {
12281260
}
12291261
});
12301262
}
1263+
if (saveButtonView != null) {
1264+
saveButtonView.setOnClickListener(v -> {
1265+
takeMediaBrowseTreeSnapshot();
1266+
});
1267+
}
1268+
1269+
}
1270+
1271+
private void takeMediaBrowseTreeSnapshot(){
1272+
if(mBrowser != null) {
1273+
if(mMediaBrowseTreeSnapshot == null) {
1274+
mMediaBrowseTreeSnapshot = new MediaBrowseTreeSnapshot(
1275+
MediaAppControllerActivity.this, mBrowser);
1276+
}
1277+
Intent saveTextFileIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
1278+
saveTextFileIntent.addCategory(Intent.CATEGORY_OPENABLE);
1279+
saveTextFileIntent.setType("text/plain");
1280+
saveTextFileIntent.putExtra(
1281+
Intent.EXTRA_TITLE, DEFAULT_BROWSE_TREE_FILE_NAME);
1282+
MediaAppControllerActivity.this.startActivityForResult(saveTextFileIntent,
1283+
CREATE_DOCUMENT_REQUEST_FOR_SNAPSHOT);
1284+
1285+
}else{
1286+
Log.e(TAG, "Media browser is null");
1287+
runOnUiThread(new Runnable() {
1288+
@Override
1289+
public void run() {
1290+
Toast.makeText(getApplicationContext(),"No media browser to snapshot",
1291+
Toast.LENGTH_SHORT).show();
1292+
}
1293+
});
1294+
}
12311295
}
12321296

12331297
protected void subscribe() {
@@ -1284,7 +1348,8 @@ private class SearchMediaItemsAdapter extends BrowseMediaItemsAdapter {
12841348
@Override
12851349
protected void subscribe() {
12861350
if (treeDepth() == 1) {
1287-
mBrowser.search(getCurrentNode(), null, new MediaBrowserCompat.SearchCallback() {
1351+
mBrowser.search(getCurrentNode(), null,
1352+
new MediaBrowserCompat.SearchCallback() {
12881353
@Override
12891354
public void onSearchResult(@NonNull String query, Bundle extras,
12901355
@NonNull List<MediaBrowserCompat.MediaItem> items) {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.example.android.mediacontroller
2+
3+
import android.content.Context
4+
import android.os.Handler
5+
import android.os.Looper
6+
import android.support.v4.media.MediaBrowserCompat
7+
import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback
8+
import android.util.Log
9+
import android.widget.Toast
10+
import androidx.lifecycle.ViewModel
11+
import androidx.lifecycle.viewModelScope
12+
import kotlinx.coroutines.launch
13+
import java.io.OutputStream
14+
import java.io.PrintWriter
15+
import kotlin.coroutines.resume
16+
import kotlin.coroutines.suspendCoroutine
17+
18+
19+
class MediaBrowseTreeSnapshot(private val context: Context, private val browser: MediaBrowserCompat):ViewModel() {
20+
private val TAG = "MediaBrowseTreeSnapshot"
21+
22+
23+
/**
24+
* Loads the browsers top level children and runs a DFS on them printing out
25+
* each media item's contentes as it is visited.
26+
*/
27+
fun takeBrowserSnapshot(outputStream: OutputStream) {
28+
29+
viewModelScope.launch {
30+
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = getChildNodes(browser.root)
31+
if (mediaItems.isNotEmpty()) {
32+
runDFSOnBrowseTree(mediaItems, outputStream)
33+
for (item in mediaItems) {
34+
Log.i(TAG, item.toString())
35+
}
36+
} else {
37+
notifyUser("No media items found, could not save tree.")
38+
}
39+
}
40+
}
41+
42+
private suspend fun getChildNodes(rootItemMid: String): MutableList<MediaBrowserCompat.MediaItem> =
43+
suspendCoroutine {
44+
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
45+
browser.subscribe(rootItemMid, object : SubscriptionCallback() {
46+
override fun onChildrenLoaded(parentId: String,
47+
children: List<MediaBrowserCompat.MediaItem>) {
48+
// Notify the main thread that all of the children have loaded
49+
mediaItems.addAll(children)
50+
super.onChildrenLoaded(parentId, children)
51+
it.resume(mediaItems)
52+
}
53+
})
54+
}
55+
56+
/**
57+
* Kicks off the browse tree depth first search by visiting all of the top level media
58+
* item nodes.
59+
*/
60+
private suspend fun runDFSOnBrowseTree(mediaItems: MutableList<MediaBrowserCompat.MediaItem>, outputStream: OutputStream) {
61+
val printWriter = PrintWriter(outputStream)
62+
printWriter.println("Root:")
63+
for (item in mediaItems) {
64+
visitMediaItemNode(item, printWriter, 1)
65+
}
66+
printWriter.flush()
67+
printWriter.close()
68+
outputStream.close()
69+
notifyUser("MediaItems saved to specified location.")
70+
}
71+
72+
/**
73+
* Visits a media item node by printing out its contents and then visiting all of its children.
74+
*/
75+
private suspend fun visitMediaItemNode(mediaItem: MediaBrowserCompat.MediaItem?, printWriter: PrintWriter, depth: Int) {
76+
if (mediaItem != null) {
77+
printMediaItemDescription(printWriter, mediaItem, depth)
78+
val mid = if (mediaItem.mediaId != null) mediaItem.mediaId!! else ""
79+
80+
// If a media item is not a leaf continue DFS on it
81+
if (mediaItem.isBrowsable && mid != "") {
82+
83+
val mediaChildren: MutableList<MediaBrowserCompat.MediaItem> = getChildNodes(mid)
84+
85+
// Run visit on all of the nodes children
86+
for (mediaItemChild in mediaChildren) {
87+
visitMediaItemNode(mediaItemChild, printWriter, depth + 1)
88+
Log.i(TAG, "Visiting:" + mediaItemChild.toString())
89+
}
90+
}
91+
}
92+
}
93+
94+
/**
95+
* Prints the contents of a media item using a print writer.
96+
*/
97+
private fun printMediaItemDescription(printWriter: PrintWriter, mediaItem: MediaBrowserCompat.MediaItem, depth: Int) {
98+
val descriptionCompat = mediaItem.description
99+
// Tab the media item to the respective depth
100+
val tabStr = String(CharArray(depth)).replace("\u0000",
101+
"\t")
102+
val titleStr = if (descriptionCompat.title != null) descriptionCompat.title.toString() else "NAN"
103+
val subTitleStr = if (descriptionCompat.subtitle != null) descriptionCompat.subtitle.toString() else "NAN"
104+
val mIDStr = if (descriptionCompat.mediaId != null) descriptionCompat.mediaId else "NAN"
105+
val uriStr = if (descriptionCompat.mediaUri != null) descriptionCompat.mediaUri.toString() else "NAN"
106+
val desStr = if (descriptionCompat.description != null) descriptionCompat.description.toString() else "NAN"
107+
val infoStr = String.format(
108+
"%sTitle:%s,Subtitle:%s,MediaId:%s,URI:%s,Description:%s",
109+
tabStr, titleStr, subTitleStr, mIDStr, uriStr, desStr)
110+
printWriter.println(infoStr)
111+
}
112+
113+
/**
114+
* Display formatted toast to user.
115+
*/
116+
private fun notifyUser(textToNotify: String) {
117+
Handler(Looper.getMainLooper()).post {
118+
val toast = Toast.makeText(
119+
context,
120+
textToNotify,
121+
Toast.LENGTH_LONG)
122+
toast.setMargin(50f, 50f)
123+
toast.show()
124+
}
125+
}
126+
}

mediacontroller/src/main/res/layout/media_browse_tree.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
android:layout_height="wrap_content"
4545
android:text="@string/media_browse_tree_up" />
4646

47+
<Button android:id="@+id/media_browse_tree_save"
48+
android:layout_width="wrap_content"
49+
android:layout_height="wrap_content"
50+
android:text="@string/media_browse_tree_save"/>
51+
4752
LinearLayout>
4853

4954
<TextView

mediacontroller/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
<string name="media_no_browser">No Browserstring>
120120
<string name="media_browse_tree_loading">Loading…string>
121121
<string name="media_browse_tree_empty">Empty.string>
122+
<string name="media_browse_tree_save">Save to filestring>
122123
<string name="search_media">Searchstring>
123124

124125
<string name="rating_thumb_up">Thumb Upstring>

0 commit comments

Comments
 (0)