Stream local files with NanoHTTPD server (#681)

Added local file browser. NanoHTTPD is used to create a local HTTP server to allow to stream files stored on in Android device to Kodi. Added support for multiple local files streaming.
This commit is contained in:
Francesco Bonazzi 2019-12-03 19:17:19 +01:00 committed by Synced Synapse
parent 83527db928
commit b14524963f
14 changed files with 1216 additions and 7 deletions

View File

@ -113,6 +113,7 @@ dependencies {
implementation 'at.blogc:expandabletextview:1.0.3'
implementation 'com.sothree.slidinguppanel:library:3.3.1'
implementation 'com.simplecityapps:recyclerview-fastscroll:1.0.20'
implementation 'org.nanohttpd:nanohttpd:2.3.1'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'

View File

@ -5,5 +5,6 @@
<!-- For espresso testing purposes, this is removed in live builds, but not in dev builds -->
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>

View File

@ -67,6 +67,7 @@
<activity android:name="org.xbmc.kore.ui.sections.addon.AddonsActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.settings.SettingsActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.file.FileActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.localfile.LocalFileActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.video.PVRActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.video.AllCastActivity"/>
<activity android:name=".ui.sections.favourites.FavouritesActivity" />

View File

@ -50,6 +50,7 @@ import org.xbmc.kore.ui.sections.audio.MusicActivity;
import org.xbmc.kore.ui.sections.favourites.FavouritesActivity;
import org.xbmc.kore.ui.sections.file.FileActivity;
import org.xbmc.kore.ui.sections.hosts.HostManagerActivity;
import org.xbmc.kore.ui.sections.localfile.LocalFileActivity;
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
import org.xbmc.kore.ui.sections.settings.SettingsActivity;
import org.xbmc.kore.ui.sections.video.MoviesActivity;
@ -82,10 +83,11 @@ public class NavigationDrawerFragment extends Fragment {
ACTIVITY_TVSHOWS = 3,
ACTIVITY_MUSIC = 4,
ACTIVITY_PVR = 5,
ACTIVITY_FILES = 6,
ACTIVITY_LOCAL_FILES = 6,
ACTIVITY_ADDONS = 7,
ACTIVITY_SETTINGS = 8,
ACTIVITY_FAVOURITES = 9;
ACTIVITY_FAVOURITES = 9,
ACTIVITY_FILES = 10;
// The current selected item id (based on the activity)
@ -149,7 +151,8 @@ public class NavigationDrawerFragment extends Fragment {
R.attr.iconFiles,
R.attr.iconAddons,
R.attr.iconSettings,
R.attr.iconFavourites
R.attr.iconFavourites,
R.attr.iconFiles
});
HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo();
@ -189,8 +192,13 @@ public class NavigationDrawerFragment extends Fragment {
styledAttributes.getResourceId(styledAttributes.getIndex(ACTIVITY_FAVOURITES), 0)));
if (shownItems.contains(String.valueOf(ACTIVITY_FILES)))
items.add(new DrawerItem(DrawerItem.TYPE_NORMAL_ITEM, ACTIVITY_FILES,
getString(R.string.files),
styledAttributes.getResourceId(styledAttributes.getIndex(ACTIVITY_FILES), 0)));
getString(R.string.files),
styledAttributes.getResourceId(styledAttributes.getIndex(ACTIVITY_FILES), 0)));
if (shownItems.contains(String.valueOf(ACTIVITY_LOCAL_FILES)))
items.add(new DrawerItem(DrawerItem.TYPE_NORMAL_ITEM, ACTIVITY_LOCAL_FILES,
getString(R.string.local_files),
styledAttributes.getResourceId(styledAttributes.getIndex(ACTIVITY_LOCAL_FILES), 0)));
if (shownItems.contains(String.valueOf(ACTIVITY_ADDONS)))
items.add(new DrawerItem(DrawerItem.TYPE_NORMAL_ITEM, ACTIVITY_ADDONS,
getString(R.string.addons),
@ -356,6 +364,8 @@ public class NavigationDrawerFragment extends Fragment {
return ACTIVITY_FAVOURITES;
else if (activity instanceof FileActivity)
return ACTIVITY_FILES;
else if (activity instanceof LocalFileActivity)
return ACTIVITY_LOCAL_FILES;
else if (activity instanceof AddonsActivity)
return ACTIVITY_ADDONS;
else if (activity instanceof SettingsActivity)
@ -369,13 +379,14 @@ public class NavigationDrawerFragment extends Fragment {
*/
private static final SparseArray<Class> activityItemIdMap;
static {
activityItemIdMap = new SparseArray<>(10);
activityItemIdMap = new SparseArray<>(11);
activityItemIdMap.put(ACTIVITY_HOSTS, HostManagerActivity.class);
activityItemIdMap.put(ACTIVITY_REMOTE, RemoteActivity.class);
activityItemIdMap.put(ACTIVITY_MOVIES, MoviesActivity.class);
activityItemIdMap.put(ACTIVITY_MUSIC, MusicActivity.class);
activityItemIdMap.put(ACTIVITY_FAVOURITES, FavouritesActivity.class);
activityItemIdMap.put(ACTIVITY_FILES, FileActivity.class);
activityItemIdMap.put(ACTIVITY_LOCAL_FILES, LocalFileActivity.class);
activityItemIdMap.put(ACTIVITY_TVSHOWS, TVShowsActivity.class);
activityItemIdMap.put(ACTIVITY_PVR, PVRActivity.class);
activityItemIdMap.put(ACTIVITY_ADDONS, AddonsActivity.class);

View File

@ -0,0 +1,119 @@
package org.xbmc.kore.ui.sections.localfile;
import android.content.Context;
import android.net.wifi.WifiManager;
import org.xbmc.kore.utils.LogUtils;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import fi.iki.elonen.NanoHTTPD;
import static android.content.Context.WIFI_SERVICE;
public class HttpApp extends NanoHTTPD {
private HttpApp(Context applicationContext, int port) throws IOException {
super(port);
this.applicationContext = applicationContext;
this.localFileLocationList = new LinkedList<>();
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}
private Context applicationContext;
private LinkedList<LocalFileLocation> localFileLocationList = null;
private int currentIndex;
@Override
public Response serve(IHTTPSession session) {
Map<String, List<String>> params = session.getParameters();
if (localFileLocationList == null) {
return newFixedLengthResponse(Response.Status.FORBIDDEN, "", "");
}
if (!params.containsKey("number")) {
return null;
}
int file_number = Integer.parseInt(params.get("number").get(0));
FileInputStream fis = null;
LocalFileLocation localFileLocation = localFileLocationList.get(file_number);
try {
fis = new FileInputStream(localFileLocation.fullPath);
} catch (FileNotFoundException e) {
LogUtils.LOGW(LogUtils.makeLogTag(HttpApp.class), e.toString());
return newFixedLengthResponse(Response.Status.FORBIDDEN, "", "");
}
String mimeType = localFileLocation.getMimeType();
return newChunkedResponse(Response.Status.OK, mimeType, fis);
}
public void addLocalFilePath(LocalFileLocation localFileLocation) {
if (localFileLocationList.contains(localFileLocation)) {
// Path already exists, get its index:
currentIndex = localFileLocationList.indexOf(localFileLocation);
} else {
this.localFileLocationList.add(localFileLocation);
currentIndex = localFileLocationList.size() - 1;
}
}
private String getIpAddress() throws UnknownHostException {
WifiManager wm = (WifiManager) applicationContext.getSystemService(WIFI_SERVICE);
byte[] byte_address = BigInteger.valueOf(wm.getConnectionInfo().getIpAddress()).toByteArray();
// Reverse `byte_address`:
for (int i = 0; i < byte_address.length/2; i++) {
byte temp = byte_address[i];
int j = byte_address.length - i - 1;
if (j < 0)
break;
byte_address[i] = byte_address[j];
byte_address[j] = temp;
}
InetAddress inet_address = InetAddress.getByAddress(byte_address);
String ip = inet_address.getHostAddress();
return ip;
}
public String getLinkToFile() {
String ip = null;
try {
ip = getIpAddress();
} catch (UnknownHostException uhe) {
return null;
}
try {
if (!isAlive())
start();
} catch (IOException ioe) {
LogUtils.LOGE(LogUtils.makeLogTag(HttpApp.class), ioe.getMessage());
}
return "http://" + ip + ":" + getListeningPort() + "/" + localFileLocationList.get(currentIndex).fileName + "?number=" + currentIndex;
}
private static HttpApp http_app = null;
public static HttpApp getInstance(Context applicationContext, int port) throws IOException {
if (http_app == null) {
synchronized (HttpApp.class) {
if (http_app == null) {
http_app = new HttpApp(applicationContext, port);
}
}
}
return http_app;
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2015 DanhDroid. All rights reserved.
* Copyright 2019 Upabjojr. All rights reserved.
*
* 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
*
* http://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 org.xbmc.kore.ui.sections.localfile;
import android.Manifest;
import android.content.pm.PackageManager;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import org.xbmc.kore.R;
import org.xbmc.kore.ui.BaseMediaActivity;
import org.xbmc.kore.ui.OnBackPressedListener;
import org.xbmc.kore.ui.sections.file.FileListFragment;
/**
* Handles listing of files fragments
*/
public class LocalFileActivity extends BaseMediaActivity {
@Override
protected String getActionBarTitle() {
return getString(R.string.local_file_browser);
}
@Override
protected Fragment createFragment() {
return new LocalFileListFragment();
}
OnBackPressedListener fragmentBackListener;
public void setBackPressedListener(OnBackPressedListener listener) {
fragmentBackListener = listener;
}
@Override
public void onBackPressed() {
// tell fragment to move up one directory
if (fragmentBackListener != null) {
boolean handled = fragmentBackListener.onBackPressed();
if (!handled)
super.onBackPressed();
}
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2019 Upabjojr. All rights reserved.
*
* 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
*
* http://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 org.xbmc.kore.ui.sections.localfile;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.content.ContextCompat;
import org.xbmc.kore.R;
import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.ui.AbstractTabsFragment;
import org.xbmc.kore.ui.OnBackPressedListener;
import org.xbmc.kore.utils.TabsAdapter;
import java.io.File;
/**
* Manages the viewpager of files
*/
public class LocalFileListFragment extends AbstractTabsFragment
implements OnBackPressedListener {
@Override
protected TabsAdapter createTabsAdapter(DataHolder dataHolder) {
ListType.Sort sortMethod = new ListType.Sort(ListType.Sort.SORT_METHOD_PATH, true, true);
Bundle dcimFileListArgs = new Bundle();
String dcim = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();
dcimFileListArgs.putString(LocalMediaFileListFragment.ROOT_PATH_LOCATION, dcim);
dcimFileListArgs.putParcelable(LocalMediaFileListFragment.SORT_METHOD, sortMethod);
Bundle directoryMusicFileListArgs = new Bundle();
String directoryMusic = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath();
directoryMusicFileListArgs.putString(LocalMediaFileListFragment.ROOT_PATH_LOCATION, directoryMusic);
directoryMusicFileListArgs.putParcelable(LocalMediaFileListFragment.SORT_METHOD, sortMethod);
Bundle directoryMoviesFileListArgs = new Bundle();
String directoryMovies = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath();
directoryMoviesFileListArgs.putString(LocalMediaFileListFragment.ROOT_PATH_LOCATION, directoryMovies);
directoryMoviesFileListArgs.putParcelable(LocalMediaFileListFragment.SORT_METHOD, sortMethod);
Bundle externalStorageFileListArgs = new Bundle();
String externalStorage = Environment.getExternalStorageDirectory().getAbsolutePath();
externalStorageFileListArgs.putString(LocalMediaFileListFragment.ROOT_PATH_LOCATION, externalStorage);
externalStorageFileListArgs.putParcelable(LocalMediaFileListFragment.SORT_METHOD, sortMethod);
TabsAdapter tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager())
.addTab(LocalMediaFileListFragment.class, dcimFileListArgs, R.string.dcim, 1)
.addTab(LocalMediaFileListFragment.class, directoryMusicFileListArgs, R.string.music, 2)
.addTab(LocalMediaFileListFragment.class, directoryMoviesFileListArgs, R.string.movies, 3)
.addTab(LocalMediaFileListFragment.class, externalStorageFileListArgs, R.string.external_storage, 4);
Environment.getRootDirectory();
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(getActivity(),null);
for (int i = 0; i < externalFilesDirs.length; i++) {
File file = externalFilesDirs[i].getParentFile().getParentFile().getParentFile().getParentFile();
if (file.getAbsolutePath().equals(externalStorage))
continue;
Bundle bundle = new Bundle();
bundle.putString(LocalMediaFileListFragment.ROOT_PATH_LOCATION, file.getAbsolutePath());
bundle.putParcelable(LocalMediaFileListFragment.SORT_METHOD, sortMethod);
tabsAdapter.addTab(LocalMediaFileListFragment.class, bundle, file.getName(),i+2);
}
return tabsAdapter;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
LocalFileActivity listenerActivity = (LocalFileActivity) activity;
listenerActivity.setBackPressedListener(this);
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " unable to register BackPressedListener");
}
}
@Override
public boolean onBackPressed() {
// Tell current fragment to move up one directory, if possible
LocalMediaFileListFragment curPage = (LocalMediaFileListFragment)((TabsAdapter)getViewPager().getAdapter())
.getStoredFragment(getViewPager().getCurrentItem());
if ((curPage != null) && !curPage.atRootDirectory()) {
curPage.onBackPressed();
return true;
}
// Not handled, let the activity handle it
return false;
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright 2015 DanhDroid. All rights reserved.
* Copyright 2019 Upabjojr. All rights reserved.
*
* 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
*
* http://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 org.xbmc.kore.ui.sections.localfile;
import android.os.Parcel;
import android.os.Parcelable;
import android.webkit.MimeTypeMap;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import java.util.regex.Pattern;
public class LocalFileLocation implements Parcelable {
public final String fileName;
public final String details;
public final String sizeDuration;
public final String fullPath;
public final boolean isDirectory;
public final boolean hasParent;
private boolean isRoot;
public boolean isRootDir() { return this.isRoot; }
public void setRootDir(boolean root) { this.isRoot = root; }
public LocalFileLocation(String fileName, String path, boolean isDir) {
this(fileName, path, isDir, null, null);
}
static final Pattern noParent = Pattern.compile("/[^/]*/?");
public LocalFileLocation(String fileName, String path, boolean isDir, String details, String sizeDuration) {
this.fileName = fileName;
this.fullPath = path;
this.isDirectory = isDir;
this.hasParent = !noParent.matcher(path).matches();
this.isRoot = false;
this.details = details;
this.sizeDuration = sizeDuration;
}
private LocalFileLocation(Parcel in) {
this.fileName = in.readString();
this.fullPath = in.readString();
this.isDirectory = (in.readInt() != 0);
this.hasParent = (in.readInt() != 0);
this.isRoot = (in.readInt() != 0);
this.details = in.readString();
this.sizeDuration = in.readString();
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel out, int flags) {
out.writeString(fileName);
out.writeString(fullPath);
out.writeInt(isDirectory ? 1 : 0);
out.writeInt(hasParent ? 1 : 0);
out.writeInt(isRoot ? 1 : 0);
out.writeString(details);
out.writeString(sizeDuration);
}
public static final Creator<LocalFileLocation> CREATOR = new Creator<LocalFileLocation>() {
public LocalFileLocation createFromParcel(Parcel in) {
return new LocalFileLocation(in);
}
public LocalFileLocation[] newArray(int size) {
return new LocalFileLocation[size];
}
};
private String getFileMimeType(final String filename) {
String extension = MimeTypeMap.getFileExtensionFromUrl(filename);
if (extension == null) {
return null;
}
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
public String getMimeType() {
return getFileMimeType(fullPath);
}
public int getPlaylistTypeId() {
String mimeType = getMimeType();
if (mimeType.matches("video.*")) {
return PlaylistType.VIDEO_PLAYLISTID;
} else if (mimeType.matches("audio.*")) {
return PlaylistType.MUSIC_PLAYLISTID;
} else if (mimeType.matches("image.*")) {
return PlaylistType.PICTURE_PLAYLISTID;
}
return -1;
}
}

View File

@ -0,0 +1,688 @@
/*
* Copyright 2015 DanhDroid. All rights reserved.
* Copyright 2019 Upabjojr. All rights reserved.
*
* 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
*
* http://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 org.xbmc.kore.ui.sections.localfile;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import com.squareup.picasso.Picasso;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.HostConnection;
import org.xbmc.kore.jsonrpc.method.Files;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.ui.AbstractListFragment;
import org.xbmc.kore.ui.viewgroups.RecyclerViewEmptyViewSupport;
import org.xbmc.kore.utils.CharacterDrawable;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* Presents a list of files of different types (Video/Music)
*/
public class LocalMediaFileListFragment extends AbstractListFragment {
private static final String TAG = LogUtils.makeLogTag(LocalMediaFileListFragment.class);
public static final String MEDIA_TYPE = "mediaType";
public static final String SORT_METHOD = "sortMethod";
public static final String PATH_CONTENTS = "pathContents";
public static final String ROOT_PATH_CONTENTS = "rootPathContents";
public static final String ROOT_VISITED = "rootVisited";
public static final String ROOT_PATH = "rootPath";
public static final String ROOT_PATH_LOCATION = "rootPath";
public static final String DELAY_LOAD = "delayLoad";
private static final String ADDON_SOURCE = "addons:";
private String rootPathLocation = null;
private HostManager hostManager;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
private LocalFileLocation currentDirectory = null;
String mediaType = Files.Media.FILES;
ListType.Sort sortMethod = null;
String parentDirectory = null;
// private MediaPictureListAdapter adapter = null;
boolean browseRootAlready = false;
LocalFileLocation loadOnVisible = null;
ArrayList<LocalFileLocation> rootFileLocation = new ArrayList<>();
Queue<LocalFileLocation> mediaQueueFileLocation = new LinkedList<>();
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(MEDIA_TYPE, mediaType);
outState.putParcelable(SORT_METHOD, sortMethod);
try {
outState.putParcelableArrayList(PATH_CONTENTS, (ArrayList<LocalFileLocation>) ((MediaPictureListAdapter) getAdapter()).getFileItemList());
} catch (NullPointerException npe) {
// adapter is null probably nothing was save in bundle because the directory is empty
// ignore this so that the empty message would display later on
}
outState.putParcelableArrayList(ROOT_PATH_CONTENTS, rootFileLocation);
outState.putBoolean(ROOT_VISITED, browseRootAlready);
}
@Override
protected RecyclerViewEmptyViewSupport.OnItemClickListener createOnItemClickListener() {
return new RecyclerViewEmptyViewSupport.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
handleFileSelect(((MediaPictureListAdapter) getAdapter()).getItem(position));
}
};
}
@Override
protected RecyclerView.Adapter createAdapter() {
return new MediaPictureListAdapter(getActivity(), R.layout.grid_item_picture);
}
@Override
public void onRefresh() {
browseDirectory(currentDirectory);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
switch (requestCode) {
case Utils.PERMISSION_REQUEST_READ_EXTERNAL_STORAGE:
// If request is cancelled, the result arrays are empty.
if ((grantResults.length > 0) &&
(grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
browseSources();
} else {
Toast.makeText(getActivity(), R.string.read_storage_permission_denied, Toast.LENGTH_SHORT)
.show();
}
break;
}
}
private boolean checkReadStoragePermission() {
boolean hasStoragePermission =
ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
if (!hasStoragePermission) {
requestPermissions(new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
Utils.PERMISSION_REQUEST_READ_EXTERNAL_STORAGE);
return false;
}
return true;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = super.onCreateView(inflater, container, savedInstanceState);
Bundle args = getArguments();
LocalFileLocation rootPath = null;
checkReadStoragePermission();
try {
if (http_app == null) {
http_app = HttpApp.getInstance(getContext(), 8080);
}
} catch (IOException ioe) {
Toast.makeText(getContext(),
getString(R.string.error_starting_http_server),
Toast.LENGTH_LONG).show();
}
if (args != null) {
rootPath = args.getParcelable(ROOT_PATH);
this.rootPathLocation = args.getString(ROOT_PATH_LOCATION);
mediaType = args.getString(MEDIA_TYPE, Files.Media.FILES);
sortMethod = args.getParcelable(SORT_METHOD);
}
hostManager = HostManager.getInstance(getActivity());
getEmptyView().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!atRootDirectory())
browseSources();
}
});
if (savedInstanceState != null) {
mediaType = savedInstanceState.getString(MEDIA_TYPE, Files.Media.FILES);
//currentPath = savedInstanceState.getString(CURRENT_PATH);
sortMethod = savedInstanceState.getParcelable(SORT_METHOD);
ArrayList<LocalFileLocation> list = savedInstanceState.getParcelableArrayList(PATH_CONTENTS);
rootFileLocation = savedInstanceState.getParcelableArrayList(ROOT_PATH_CONTENTS);
browseRootAlready = savedInstanceState.getBoolean(ROOT_VISITED);
((MediaPictureListAdapter) getAdapter()).setFilelistItems(list);
}
else if (rootPath != null) {
loadOnVisible = rootPath;
// setUserVisibleHint may have already fired
setUserVisibleHint(getUserVisibleHint() || !args.getBoolean(DELAY_LOAD, false));
}
else {
browseSources();
}
return root;
}
@Override
public void setUserVisibleHint (boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && loadOnVisible != null) {
LocalFileLocation rootPath = loadOnVisible;
loadOnVisible = null;
browseRootAlready = true;
browseDirectory(rootPath);
}
}
void handleFileSelect(LocalFileLocation f) {
// if selection is a directory, browse the the level below
if (f.isDirectory) {
// a directory - store the path of this directory so that we can reverse travel if
// we want to
if (f.isRootDir()) {
if (browseRootAlready)
browseDirectory(f);
else {
browseSources();
}
} else {
browseDirectory(f);
}
} else {
playMediaFile(f);
}
}
public void onBackPressed() {
// Emulate a click on ..
handleFileSelect(((MediaPictureListAdapter) getAdapter()).getItem(0));
}
public boolean atRootDirectory() {
return currentDirectory.fullPath.equals(rootPathLocation);
}
/**
* Gets and presents the list of media sources
*/
private void browseSources() {
File directory = null;
if (rootFileLocation != null) {
directory = new File(rootPathLocation);
} else {
return;
}
File[] files = directory.listFiles();
if (files == null) {
Toast.makeText(getActivity(),
getString(R.string.error_reading_local_storage),
Toast.LENGTH_LONG).show();
return;
}
Arrays.sort(files);
rootFileLocation.clear();
browseRootAlready = true;
for (File file : files) {
boolean isDir = file.isDirectory();
rootFileLocation.add(
new LocalFileLocation(file.getName(), file.getAbsolutePath(), isDir)
);
}
((MediaPictureListAdapter) getAdapter()).setFilelistItems(rootFileLocation);
}
/**
* Gets and presents the files of the specified directory
* @param dir Directory to browse
*/
private void browseDirectory(final LocalFileLocation dir) {
if (dir.isRootDir()) {
// this is a root directory
parentDirectory = dir.fullPath;
} else {
// check to make sure that this is not our root path
String rootPath = null;
String path;
for (LocalFileLocation fl : rootFileLocation) {
path = fl.fullPath;
if ((path != null) && (dir.fullPath != null) &&
(dir.fullPath.contentEquals(path))) {
rootPath = fl.fullPath;
break;
}
}
if (rootPath != null) {
parentDirectory = rootPath;
dir.setRootDir(true);
} else if (dir.fullPath != null) {
parentDirectory = getParentDirectory(dir.fullPath);
}
}
currentDirectory = dir;
File[] files = (new File(dir.fullPath)).listFiles();
if (files == null) {
Toast.makeText(
getContext(),
String.format(getString(R.string.error_getting_source_info), "listFiles() failed"),
Toast.LENGTH_LONG).show();
return;
}
Arrays.sort(files);
ArrayList<LocalFileLocation> dir_list = new ArrayList<>();
ArrayList<LocalFileLocation> file_list = new ArrayList<>();
if (dir.hasParent) {
// insert the parent directory as the first item in the list
parentDirectory = getParentDirectory(dir.fullPath);
LocalFileLocation fl = new LocalFileLocation("..", parentDirectory, true);
fl.setRootDir(dir.isRootDir());
dir_list.add(0, fl);
}
for (File file : files) {
LocalFileLocation fl = new LocalFileLocation(file.getName(), file.getAbsolutePath(), file.isDirectory());
if (fl.isDirectory) {
dir_list.add(fl);
} else {
file_list.add(fl);
}
}
// TODO: use sortMethod here
ArrayList<LocalFileLocation> full_list = new ArrayList<>();
full_list.addAll(dir_list);
full_list.addAll(file_list);
((MediaPictureListAdapter) getAdapter()).setFilelistItems(full_list);
browseRootAlready = false;
}
/**
* Starts playing the given media file
* @param localFileLocation LocalFileLocation to start playing
*/
private void playMediaFile(final LocalFileLocation localFileLocation) {
http_app.addLocalFilePath(localFileLocation);
String url = http_app.getLinkToFile();
PlaylistType.Item item = new PlaylistType.Item();
item.file = url;
Player.Open action = new Player.Open(item);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
while (!mediaQueueFileLocation.isEmpty()) {
queueMediaFile(mediaQueueFileLocation.poll());
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.error_play_local_file), description),
Toast.LENGTH_SHORT).show();
}
}, callbackHandler);
}
/**
* Queues the given media file on the active playlist, and starts it if nothing is playing
* @param localFileLocation LocalFileLocation to queue
*/
private void queueMediaFile(final LocalFileLocation localFileLocation) {
http_app.addLocalFilePath(localFileLocation);
String url = http_app.getLinkToFile();
final HostConnection connection = hostManager.getConnection();
PlaylistType.Item item = new PlaylistType.Item();
item.file = url;
Playlist.Add action = new Playlist.Add(localFileLocation.getPlaylistTypeId(), item);
action.execute(connection, new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
startPlaylistIfNoActivePlayers(connection, localFileLocation.getPlaylistTypeId(), callbackHandler);
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.error_queue_media_file), description),
Toast.LENGTH_SHORT).show();
}
}, callbackHandler);
}
/**
* Starts a playlist if no active players are playing
* @param connection Host connection
* @param playlistId PlaylistId to start
* @param callbackHandler Handler on which to post method callbacks
*/
private void startPlaylistIfNoActivePlayers(final HostConnection connection,
final int playlistId,
final Handler callbackHandler) {
Player.GetActivePlayers action = new Player.GetActivePlayers();
action.execute(connection, new ApiCallback<ArrayList<PlayerType.GetActivePlayersReturnType>>() {
@Override
public void onSuccess(ArrayList<PlayerType.GetActivePlayersReturnType> result ) {
// find out if any player is running. If it is not, start one
if (result.isEmpty()) {
Player.Open action = new Player.Open(Player.Open.TYPE_PLAYLIST, playlistId);
action.execute(connection, new ApiCallback<String>() {
@Override
public void onSuccess(String result) { }
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.error_play_media_file), description),
Toast.LENGTH_SHORT).show();
}
}, callbackHandler);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.error_get_active_player), description),
Toast.LENGTH_SHORT).show();
}
}, callbackHandler);
}
/**
* return the path of the parent based on path
* @param path of the current media file
* @return path of the parent
*/
public static String getParentDirectory(final String path) {
String p = path;
String pathSymbol = "/"; // unix style
if (path.contains("\\")) {
pathSymbol = "\\"; // windows style
}
// if path ends with /, remove it before removing the directory name
if (path.endsWith(pathSymbol)) {
p = path.substring(0, path.length() - 1);
}
if (p.lastIndexOf(pathSymbol) != -1) {
p = p.substring(0, p.lastIndexOf(pathSymbol));
}
p = p + pathSymbol; // add it back to make it look like path
return p;
}
/**
* return the filename of a given path, if path is a directory, return directory name
* @param path of the current file
* @return filename or directory name
*/
public static String getFilenameFromPath(final String path) {
String p = path;
String pathSymbol = "/"; // unix style
if (path.contains("\\")) {
pathSymbol = "\\"; // windows style
}
// if path ends with /, remove it
if (path.endsWith(pathSymbol)) {
p = path.substring(0, path.length() - 1);
}
if (p.lastIndexOf(pathSymbol) != -1) {
p = p.substring(p.lastIndexOf(pathSymbol)+1);
}
return p;
}
private class MediaPictureListAdapter extends RecyclerView.Adapter {
Context ctx;
int resource;
List<LocalFileLocation> fileLocationItems;
int artWidth;
int artHeight;
private View.OnClickListener itemMenuClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
final int position = (Integer)v.getTag();
if (fileLocationItems != null) {
final LocalFileLocation loc = fileLocationItems.get(position);
if (!loc.isDirectory) {
final PopupMenu popupMenu = new PopupMenu(getActivity(), v);
popupMenu.getMenuInflater().inflate(R.menu.filelist_item, popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_queue_item:
queueMediaFile(loc);
return true;
case R.id.action_play_item:
playMediaFile(loc);
return true;
case R.id.action_play_from_this_item:
mediaQueueFileLocation.clear();
LocalFileLocation fl;
// start playing the selected one, then queue the rest
mediaQueueFileLocation.add(loc);
for (int i = position + 1; i < fileLocationItems.size(); i++) {
fl = fileLocationItems.get(i);
if (!fl.isDirectory) {
mediaQueueFileLocation.add(fl);
}
}
playMediaFile(loc);
return true;
}
return false;
}
});
popupMenu.show();
}
}
}
};
MediaPictureListAdapter(Context context, int resource) {
super();
this.ctx = context;
this.resource = resource;
this.fileLocationItems = null;
// Get the art dimensions
Resources resources = context.getResources();
artWidth = (int)(resources.getDimension(R.dimen.picturelist_art_width) /
UIUtils.IMAGE_RESIZE_FACTOR);
artHeight = (int)(resources.getDimension(R.dimen.picturelist_art_heigth) /
UIUtils.IMAGE_RESIZE_FACTOR);
}
/**
* Manually set the items on the adapter
* Calls notifyDataSetChanged()
*
* @param items list of files/directories
*/
public void setFilelistItems(List<LocalFileLocation> items) {
this.fileLocationItems = items;
notifyDataSetChanged();
}
public List<LocalFileLocation> getFileItemList() {
if (fileLocationItems == null)
return new ArrayList<>();
return new ArrayList<>(fileLocationItems);
}
public LocalFileLocation getItem(int position) {
if (fileLocationItems == null) {
return null;
} else {
return fileLocationItems.get(position);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(ctx)
.inflate(resource, parent, false);
return new ViewHolder(view, getContext(), hostManager, artWidth, artHeight, itemMenuClickListener);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
LocalFileLocation fileLocation = this.getItem(position);
((ViewHolder) holder).bindView(fileLocation, position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
if (fileLocationItems == null) {
return 0;
} else {
return fileLocationItems.size();
}
}
}
static boolean checkFileIsPicture(File file_path) {
if ((file_path.getAbsolutePath().toLowerCase().endsWith(".jpg")) ||
(file_path.getAbsolutePath().toLowerCase().endsWith(".jpeg"))) {
return true;
}
return false;
}
/**
* View holder pattern
*/
private static class ViewHolder extends RecyclerView.ViewHolder {
ImageView art;
TextView title;
TextView details;
TextView sizeDuration;
ImageView contextMenu;
HostManager hostManager;
int artWidth;
int artHeight;
Context context;
ViewHolder(View itemView, Context context, HostManager hostManager, int artWidth, int artHeight,
View.OnClickListener itemMenuClickListener) {
super(itemView);
this.context = context;
this.hostManager = hostManager;
this.artWidth = artWidth;
this.artHeight = artHeight;
art = itemView.findViewById(R.id.art);
title = itemView.findViewById(R.id.title);
details = itemView.findViewById(R.id.details);
contextMenu = itemView.findViewById(R.id.list_context_menu);
sizeDuration = itemView.findViewById(R.id.size_duration);
contextMenu.setOnClickListener(itemMenuClickListener);
}
public void bindView(LocalFileLocation fileLocation, int position) {
title.setText(fileLocation.fileName);
details.setText(fileLocation.details);
sizeDuration.setText(fileLocation.sizeDuration);
CharacterDrawable avatarDrawable = UIUtils.getCharacterAvatar(context, fileLocation.fileName);
File file_path = new File(fileLocation.fullPath);
Picasso.with(context)
.load(file_path)
.placeholder(avatarDrawable)
.resize(artWidth, artHeight)
.centerCrop()
.into(art);
// For the popup menu
if (fileLocation.isDirectory) {
contextMenu.setVisibility(View.GONE);
} else {
contextMenu.setVisibility(View.VISIBLE);
contextMenu.setTag(position);
}
}
}
HttpApp http_app = null;
}

View File

@ -48,7 +48,8 @@ import java.util.Locale;
public class Utils {
public static final int PERMISSION_REQUEST_WRITE_STORAGE = 0,
PERMISSION_REQUEST_READ_PHONE_STATE = 1;
PERMISSION_REQUEST_READ_PHONE_STATE = 1,
PERMISSION_REQUEST_READ_EXTERNAL_STORAGE = 2;
/**
* Returns whether the SDK is the Jellybean release or later.

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015 Synced Synapse. All rights reserved.
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
http://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.
-->
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.CardView">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/art"
android:layout_width="@dimen/picturelist_art_width"
android:layout_height="@dimen/picturelist_art_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/size_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
style="@style/TextAppearance.Medialist.OtherInfo"
android:paddingLeft="@dimen/small_padding"
android:paddingStart="@dimen/small_padding"/>
<ImageView
android:id="@+id/list_context_menu"
android:layout_width="@dimen/default_icon_size"
android:layout_height="@dimen/default_icon_size"
android:padding="@dimen/default_icon_padding"
android:layout_alignTop="@id/art"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconOverflow"
android:contentDescription="@string/action_options"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/art"
android:layout_toEndOf="@+id/art"
android:layout_toLeftOf="@id/list_context_menu"
android:layout_toStartOf="@id/list_context_menu"
android:layout_alignTop="@id/art"
style="@style/TextAppearance.Medialist.Title"
android:paddingRight="@dimen/small_padding"
android:paddingEnd="@dimen/small_padding"
android:ellipsize="middle"/>
<TextView
android:id="@+id/details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/title"
android:layout_alignStart="@id/title"
android:layout_below="@id/title"
android:layout_toRightOf="@id/art"
android:layout_toEndOf="@id/art"
android:layout_toLeftOf="@id/size_duration"
android:layout_toStartOf="@id/size_duration"
style="@style/TextAppearance.Medialist.Details"
android:paddingRight="@dimen/small_padding"
android:paddingEnd="@dimen/small_padding"/>
</RelativeLayout>
</android.support.v7.widget.CardView>

View File

@ -64,6 +64,7 @@
<item>@string/pvr</item>
<item>@string/favourites</item>
<item>@string/files</item>
<item>@string/local_files</item>
<item>@string/addons</item>
</string-array>
@ -113,6 +114,7 @@
<item>5</item>
<item>9</item>
<item>6</item>
<item>10</item>
<item>7</item>
</string-array>

View File

@ -123,6 +123,9 @@
<dimen name="filelist_art_width">52dp</dimen>
<