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:
parent
83527db928
commit
b14524963f
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -123,6 +123,9 @@
|
|||
<dimen name="filelist_art_width">52dp</dimen>
|
||||
<dimen name="filelist_art_heigth">76dp</dimen>
|
||||
|
||||
<dimen name="picturelist_art_width">126dp</dimen>
|
||||
<dimen name="picturelist_art_heigth">76dp</dimen>
|
||||
|
||||
<dimen name="channellist_art_width">74dp</dimen>
|
||||
<dimen name="channellist_art_heigth">74dp</dimen>
|
||||
|
||||
|
|
|
@ -32,8 +32,11 @@
|
|||
<string name="tv_shows">TV Shows</string>
|
||||
<string name="music">Music</string>
|
||||
<string name="pictures">Pictures</string>
|
||||
<string name="dcim">DCIM</string>
|
||||
<string name="external_storage">Storage</string>
|
||||
<string name="addons">Addons</string>
|
||||
<string name="files">Files</string>
|
||||
<string name="local_files">Local Files</string>
|
||||
<string name="video">Video</string>
|
||||
<string name="videos">Videos</string>
|
||||
<string name="media">Media</string>
|
||||
|
@ -41,6 +44,7 @@
|
|||
<string name="script">Script</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="file_browser">File Browser</string>
|
||||
<string name="local_file_browser">Local File Browser</string>
|
||||
<string name="pvr">PVR</string>
|
||||
<string name="favourites">Favourites</string>
|
||||
<string name="weather">Weather</string>
|
||||
|
@ -220,11 +224,16 @@
|
|||
%1$s.</string>
|
||||
<string name="error_play_media_file">Couldn\'t play media file.\nError message:
|
||||
%1$s.</string>
|
||||
<string name="error_play_local_file">Couldn\'t play local media file.\nError message:
|
||||
%1$s.</string>
|
||||
<string name="error_queue_media_file">Couldn\'t queue media file.\nError message:
|
||||
%1$s.</string>
|
||||
<string name="error_get_active_player">Couldn\'t get active player.\nError message:
|
||||
%1$s.</string>
|
||||
<string name="error_share_video">Couldn\'t share video to Kodi/XBMC.</string>
|
||||
<string name="error_reading_local_storage">Couldn\'t read the local storage.
|
||||
Did you give Kore permission to access file storage?</string>
|
||||
<string name="error_starting_http_server">Couldn\'t create or start HTTP server.</string>
|
||||
|
||||
<string name="directors">Directors:</string>
|
||||
<string name="studio">Studio:</string>
|
||||
|
@ -414,6 +423,7 @@
|
|||
|
||||
<string name="read_phone_state_permission_denied">Permission denied. Won\'t be able to pause playback during calls.</string>
|
||||
<string name="write_storage_permission_denied">Permission denied. Won\'t be able to download files.</string>
|
||||
<string name="read_storage_permission_denied">Permission denied. Won\'t be able to read files.</string>
|
||||
|
||||
<string name="no_songs_to_download">No songs to download</string>
|
||||
<string name="by_album">By album</string>
|
||||
|
|
Loading…
Reference in New Issue