Merge branch 'develop' into iadeelzafar/wifi-hotspot

This commit is contained in:
Adeel Zafar 2019-08-16 20:15:35 +05:00
commit 7972774e2f
18 changed files with 516 additions and 639 deletions

View File

@ -0,0 +1,43 @@
/*
* Kiwix Android
* Copyright (C) 2018 Kiwix <android.kiwix.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.mhutti1.utils.storage
import java.text.DecimalFormat
const val Kb = 1 * 1024L
const val Mb = Kb * 1024
const val Gb = Mb * 1024
const val Tb = Gb * 1024
const val Pb = Tb * 1024
const val Eb = Pb * 1024
inline class Bytes(val size: Long) {
val humanReadable
get() = when {
size < Kb -> "${floatForm(size)} byte"
size < Mb -> "${floatForm(size / Kb)} KB"
size < Gb -> "${floatForm(size / Mb)} MB"
size < Tb -> "${floatForm(size / Gb)} GB"
size < Pb -> "${floatForm(size / Tb)} TB"
size < Eb -> "${floatForm(size / Pb)} PB"
size >= Eb -> "${floatForm(size / Eb)} EB"
else -> "???"
}
private fun floatForm(d: Long) = DecimalFormat("#.#").format(d.toDouble())
}

View File

@ -1,59 +0,0 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage;
import android.annotation.SuppressLint;
class ExternalPaths {
@SuppressLint("SdCardPath") private static final String[] paths = {
"/storage/sdcard0",
"/storage/sdcard1",
"/storage/extsdcard",
"/storage/extSdCard",
"/storage/sdcard0/external_sdcard",
"/mnt/sdcard/external_sd",
"/mnt/external_sd",
"/mnt/media_rw/*",
"/removable/microsd",
"/mnt/emmc",
"/storage/external_SD",
"/storage/ext_sd",
"/storage/removable/sdcard1",
"/data/sdext",
"/data/sdext2",
"/data/sdext3",
"/data/sdext2",
"/data/sdext3",
"/data/sdext4",
"/sdcard",
"/sdcard1",
"/sdcard2",
"/storage/microsd",
"/mnt/extsd",
"/extsd",
"/mnt/sdcard",
"/misc/android",
};
public static String[] getPossiblePaths() {
return paths;
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage
import android.annotation.SuppressLint
internal object ExternalPaths {
@SuppressLint("SdCardPath")
val possiblePaths = arrayOf(
"/storage/sdcard0",
"/storage/sdcard1",
"/storage/extsdcard",
"/storage/extSdCard",
"/storage/sdcard0/external_sdcard",
"/mnt/sdcard/external_sd",
"/mnt/external_sd",
"/mnt/media_rw/*",
"/removable/microsd",
"/mnt/emmc",
"/storage/external_SD",
"/storage/ext_sd",
"/storage/removable/sdcard1",
"/data/sdext",
"/data/sdext2",
"/data/sdext3",
"/data/sdext2",
"/data/sdext3",
"/data/sdext4",
"/sdcard",
"/sdcard1",
"/sdcard2",
"/storage/microsd",
"/mnt/extsd",
"/extsd",
"/mnt/sdcard",
"/misc/android"
)
}

View File

@ -1,164 +0,0 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage;
import android.os.Build;
import android.os.StatFs;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DecimalFormat;
public class StorageDevice {
// File object containing device path
private final File mFile;
private final boolean mInternal;
private boolean mDuplicate = true;
public StorageDevice(String path, boolean internal) {
mFile = new File(path);
mInternal = internal;
if (mFile.exists()) {
createLocationCode();
}
}
public StorageDevice(File file, boolean internal) {
mFile = file;
mInternal = internal;
if (mFile.exists()) {
createLocationCode();
}
}
// Get device path
public String getName() {
return mFile.getPath();
}
// Get available space on device
public String getSize() {
return bytesToHuman(getAvailableBytes());
}
private Long getAvailableBytes() {
StatFs statFs = new StatFs(mFile.getPath());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong();
} else {
return (long) statFs.getBlockSize() * (long) statFs.getAvailableBlocks();
}
}
public String getTotalSize() {
return bytesToHuman(getTotalBytes());
}
// Get total space on device
private Long getTotalBytes() {
StatFs statFs = new StatFs((mFile.getPath()));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return statFs.getBlockSizeLong() * statFs.getBlockCountLong();
} else {
return (long) statFs.getBlockSize() * (long) statFs.getBlockCount();
}
}
// Convert bytes to human readable form
private static String bytesToHuman(long size) {
long Kb = 1 * 1024;
long Mb = Kb * 1024;
long Gb = Mb * 1024;
long Tb = Gb * 1024;
long Pb = Tb * 1024;
long Eb = Pb * 1024;
if (size < Kb) return floatForm(size) + " byte";
if (size >= Kb && size < Mb) return floatForm((double) size / Kb) + " KB";
if (size >= Mb && size < Gb) return floatForm((double) size / Mb) + " MB";
if (size >= Gb && size < Tb) return floatForm((double) size / Gb) + " GB";
if (size >= Tb && size < Pb) return floatForm((double) size / Tb) + " TB";
if (size >= Pb && size < Eb) return floatForm((double) size / Pb) + " PB";
if (size >= Eb) return floatForm((double) size / Eb) + " EB";
return "???";
}
public boolean isInternal() {
return mInternal;
}
public File getPath() {
return mFile;
}
private static String floatForm(double d) {
return new DecimalFormat("#.#").format(d);
}
// Create unique file to identify duplicate devices.
private void createLocationCode() {
if (!getLocationCodeFromFolder(mFile)) {
File locationCode = new File(mFile.getPath(), ".storageLocationMarker");
try {
locationCode.createNewFile();
FileWriter fw = new FileWriter(locationCode);
fw.write(mFile.getPath());
fw.close();
} catch (IOException e) {
Log.d("android-storage-devices", "Unable to create marker file, duplicates may be listed");
}
}
}
// Check if there is already a device code in our path
private boolean getLocationCodeFromFolder(File folder) {
File locationCode = new File(folder.getPath(), ".storageLocationMarker");
if (locationCode.exists()) {
try ( BufferedReader br = new BufferedReader(new FileReader(locationCode))){
if (br.readLine().equals(mFile.getPath())) {
mDuplicate = false;
} else {
mDuplicate = true;
return true;
}
} catch (Exception e) {
return true;
}
}
String path = folder.getPath();
String parent = path.substring(0, path.lastIndexOf("/"));
if (parent.equals("")) {
mDuplicate = false;
return false;
}
return getLocationCodeFromFolder(new File(parent));
}
public boolean isDuplicate() {
return mDuplicate;
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage
import android.os.Build
import android.os.StatFs
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.FileWriter
const val LOCATION_EXTENSION = "storageLocationMarker"
data class StorageDevice(
val file: File,
val isInternal: Boolean
) {
constructor(path: String, internal: Boolean) : this(File(path), internal)
init {
if (file.exists()) {
createLocationCode()
}
}
var isDuplicate = false
private set
val name: String
get() = file.path
val availableSpace: String
get() = Bytes(availableBytes).humanReadable
private val availableBytes: Long
get() = StatFs(file.path).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
it.blockSizeLong * it.availableBlocksLong
else
it.blockSize.toLong() * it.availableBlocks.toLong()
}
val totalSize: String
get() = Bytes(totalBytes).humanReadable
// Get total space on device
private val totalBytes: Long
get() = StatFs(file.path).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
it.blockSizeLong * it.blockCountLong
else
it.blockSize.toLong() * it.blockCount.toLong()
}
// Create unique file to identify duplicate devices.
private fun createLocationCode() {
if (!getLocationCodeFromFolder(file)) {
File(file.path, ".$LOCATION_EXTENSION").let { locationCode ->
locationCode.createNewFile()
FileWriter(locationCode).use { it.write(file.path) }
}
}
}
// Check if there is already a device code in our path
private fun getLocationCodeFromFolder(folder: File): Boolean {
val locationCode = File(folder.path, ".$LOCATION_EXTENSION")
if (locationCode.exists()) {
try {
BufferedReader(FileReader(locationCode)).use { br ->
if (br.readLine() == file.path) {
isDuplicate = false
} else {
isDuplicate = true
return@getLocationCodeFromFolder true
}
}
} catch (e: Exception) {
return true
}
}
val parent = folder.parentFile
if (parent == null) {
isDuplicate = false
return false
}
return getLocationCodeFromFolder(parent)
}
}

View File

@ -1,131 +0,0 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage;
import android.content.Context;
import android.os.Environment;
import androidx.core.content.ContextCompat;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
public class StorageDeviceUtils {
public static ArrayList<StorageDevice> getStorageDevices(Context context, boolean writable) {
ArrayList<StorageDevice> storageDevices = new ArrayList<>();
// Add as many possible mount points as we know about
// Only add this device if its very likely that we have missed a users sd card
if (Environment.isExternalStorageEmulated()) {
// This is our internal storage directory
storageDevices.add(new StorageDevice(
generalisePath(Environment.getExternalStorageDirectory().getPath(), writable), true));
} else {
// This is an external storage directory
storageDevices.add(new StorageDevice(
generalisePath(Environment.getExternalStorageDirectory().getPath(), writable), false));
}
// These are possible manufacturer sdcard mount points
String[] paths = ExternalPaths.getPossiblePaths();
for (String path : paths) {
if (path.endsWith("*")) {
File root = new File(path.substring(0, path.length() - 1));
File[] directories = root.listFiles(file -> file.isDirectory());
if (directories != null) {
for (File dir : directories) {
storageDevices.add(new StorageDevice(dir, false));
}
}
} else {
storageDevices.add(new StorageDevice(path, false));
}
}
// Iterate through any sdcards manufacturers may have specified
for (File file : ContextCompat.getExternalFilesDirs(context, "")) {
if (file != null) {
storageDevices.add(new StorageDevice(generalisePath(file.getPath(), writable), false));
}
}
// Check all devices exist, we can write to them if required and they are not duplicates
return checkStorageValid(writable, storageDevices);
}
// Remove app specific path from directories so that we can search them from the top
private static String generalisePath(String path, boolean writable) {
if (writable) {
return path;
}
int endIndex = path.lastIndexOf("/Android/data/");
if (endIndex != -1) {
return path.substring(0, endIndex);
}
return path;
}
private static ArrayList<StorageDevice> checkStorageValid(boolean writable,
ArrayList<StorageDevice> storageDevices) {
ArrayList<StorageDevice> activeDevices = new ArrayList<>();
ArrayList<StorageDevice> devicePaths = new ArrayList<>();
for (StorageDevice device : storageDevices) {
if (existsAndIsDirAndWritableIfRequiredAndNotDuplicate(writable, devicePaths, device)) {
activeDevices.add(device);
devicePaths.add(device);
}
}
return activeDevices;
}
private static boolean existsAndIsDirAndWritableIfRequiredAndNotDuplicate(boolean writable,
ArrayList<StorageDevice> devicePaths, StorageDevice device) {
final File devicePath = device.getPath();
return devicePath.exists()
&& devicePath.isDirectory()
&& (canWrite(devicePath) || !writable)
&& !device.isDuplicate()
&& !devicePaths.contains(device);
}
// Amazingly file.canWrite() does not always return the correct value
private static boolean canWrite(File file) {
final String filePath = file + "/test.txt";
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
FileChannel fileChannel = randomAccessFile.getChannel();
FileLock fileLock = fileChannel.lock();
fileLock.release();
fileChannel.close();
randomAccessFile.close();
return true;
} catch (Exception ex) {
return false;
} finally {
new File(filePath).delete();
}
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage
import android.content.Context
import android.os.Environment
import androidx.core.content.ContextCompat
import java.io.File
import java.io.FileFilter
import java.io.RandomAccessFile
import java.util.ArrayList
object StorageDeviceUtils {
@JvmStatic
fun getStorageDevices(context: Context, writable: Boolean): List<StorageDevice> {
val storageDevices = ArrayList<StorageDevice>().apply {
add(environmentDevices(writable))
addAll(externalMountPointDevices())
addAll(externalFilesDirsDevices(context, writable))
}
return validate(storageDevices, writable)
}
private fun externalFilesDirsDevices(
context: Context,
writable: Boolean
) = ContextCompat.getExternalFilesDirs(context, "")
.filterNotNull()
.map { dir -> StorageDevice(generalisePath(dir.path, writable), false) }
private fun externalMountPointDevices(): Collection<StorageDevice> =
ExternalPaths.possiblePaths.fold(mutableListOf(), { acc, path ->
acc.apply {
if (path.endsWith("*")) {
addAll(devicesBeneath(File(path.substringBeforeLast("*"))))
} else {
add(StorageDevice(path, false))
}
}
})
private fun devicesBeneath(directory: File) =
directory.listFiles(FileFilter(File::isDirectory))
?.map { dir -> StorageDevice(dir, false) }
.orEmpty()
private fun environmentDevices(
writable: Boolean
) =
StorageDevice(
generalisePath(Environment.getExternalStorageDirectory().path, writable),
Environment.isExternalStorageEmulated()
)
// Remove app specific path from directories so that we can search them from the top
private fun generalisePath(path: String, writable: Boolean) =
if (writable) path
else path.substringBefore("/Android/data/")
// Amazingly file.canWrite() does not always return the correct value
private fun canWrite(file: File): Boolean = "$file/test.txt".let {
try {
RandomAccessFile(it, "rw").use { randomAccessFile ->
randomAccessFile.channel.use { channel ->
channel.lock().use { fileLock ->
fileLock.release()
true
}
}
}
} catch (ignore: Exception) {
false
} finally {
File(it).delete()
}
}
private fun validate(
storageDevices: ArrayList<StorageDevice>,
writable: Boolean
) = storageDevices.asSequence().distinct()
.filter { it.file.exists() }
.filter { it.file.isDirectory }
.filter { canWrite(it.file) || !writable }
.filterNot(StorageDevice::isDuplicate)
.toList()
.also(StorageDeviceUtils::deleteStorageMarkers)
private fun deleteStorageMarkers(validatedDevices: List<StorageDevice>) {
validatedDevices.forEach { recursiveDeleteStorageMarkers(it.file) }
}
private fun recursiveDeleteStorageMarkers(file: File) {
file.listFiles().forEach {
when {
it.isDirectory -> recursiveDeleteStorageMarkers(it)
it.extension == LOCATION_EXTENSION -> it.delete()
}
}
}
}

View File

@ -1,72 +0,0 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import org.kiwix.kiwixmobile.R;
class StorageSelectArrayAdapter extends ArrayAdapter<StorageDevice> {
private final String mInternal;
private final String mExternal;
public StorageSelectArrayAdapter(Context context, int resource, ArrayList<StorageDevice> devices,
String internal, String external) {
super(context, resource, devices);
mInternal = internal;
mExternal = external;
}
@SuppressLint("SetTextI18n") @Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = View.inflate(getContext(), R.layout.device_item, null);
holder = new ViewHolder();
holder.fileName = convertView.findViewById(R.id.file_name);
holder.fileSize = convertView.findViewById(R.id.file_size);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
StorageDevice device = getItem(position);
if (device.isInternal()) {
holder.fileName.setText(mInternal);
} else {
holder.fileName.setText(mExternal);
}
holder.fileSize.setText(device.getSize() + " / " + device.getTotalSize());
return convertView;
}
class ViewHolder {
TextView fileName;
TextView fileSize;
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.device_item.file_name
import kotlinx.android.synthetic.main.device_item.file_size
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.extensions.inflate
internal class StorageSelectArrayAdapter(
context: Context,
devices: List<StorageDevice>
) : ArrayAdapter<StorageDevice>(context, 0, devices) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var convertView = convertView
val holder: ViewHolder
if (convertView == null) {
convertView = parent.inflate(R.layout.device_item, false)
holder = ViewHolder(convertView)
convertView.tag = holder
} else {
holder = convertView.tag as ViewHolder
}
holder.bind(getItem(position)!!)
return convertView
}
@SuppressLint("SetTextI18n")
internal inner class ViewHolder(override val containerView: View) : LayoutContainer {
fun bind(device: StorageDevice) {
file_name.setText(if (device.isInternal) string.internal_storage else string.external_storage)
file_size.text = device.availableSpace + " / " + device.totalSize
}
}
}

View File

@ -1,114 +0,0 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import java.io.File;
import org.kiwix.kiwixmobile.R;
public class StorageSelectDialog extends DialogFragment implements ListView.OnItemClickListener {
// Activities/Fragments can create instances of a StorageSelectDialog and bind a listener to get its result
public static final String STORAGE_DIALOG_THEME = "THEME";
public static final String STORAGE_DIALOG_INTERNAL = "INTERNAL";
public static final String STORAGE_DIALOG_EXTERNAL = "EXTERNAL";
private StorageSelectArrayAdapter mAdapter;
private OnSelectListener mOnSelectListener;
private String mTitle;
private String mInternal = "Internal";
private String mExternal = "External";
@Override
public void onCreate(Bundle savedInstanceState) {
if (getArguments() != null) {
// Set string values
mInternal = getArguments().getString(STORAGE_DIALOG_INTERNAL, mInternal);
mExternal = getArguments().getString(STORAGE_DIALOG_EXTERNAL, mExternal);
// Set the theme to a supplied value
if (getArguments().containsKey(STORAGE_DIALOG_THEME)) {
setStyle(DialogFragment.STYLE_NORMAL, getArguments().getInt(STORAGE_DIALOG_THEME));
}
}
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.storage_select_dialog, container, false);
TextView title = rootView.findViewById(R.id.title);
title.setText(mTitle);
ListView listView = rootView.findViewById(R.id.device_list);
mAdapter = new StorageSelectArrayAdapter(getActivity(), 0,
StorageDeviceUtils.getStorageDevices(getActivity(), true), mInternal, mExternal);
listView.setAdapter(mAdapter);
listView.setOnItemClickListener(this);
Button button = rootView.findViewById(R.id.button);
final EditText editText = rootView.findViewById(R.id.editText);
button.setOnClickListener(view -> {
if (editText.getText().length() != 0) {
String path = editText.getText().toString();
if (new File(path).exists()) {
mAdapter.add(new StorageDevice(path, false));
}
}
});
return rootView;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mOnSelectListener != null) {
mOnSelectListener.selectionCallback(mAdapter.getItem(position));
}
dismiss();
}
public void setOnSelectListener(OnSelectListener selectListener) {
mOnSelectListener = selectListener;
}
public interface OnSelectListener {
void selectionCallback(StorageDevice s);
}
@Override
public void show(FragmentManager fm, String text) {
mTitle = text;
super.show(fm, text);
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2016 Isaac Hutt <mhutti1@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package eu.mhutti1.utils.storage
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView.OnItemClickListener
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.storage_select_dialog.device_list
import kotlinx.android.synthetic.main.storage_select_dialog.title
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.utils.StyleUtils
class StorageSelectDialog : DialogFragment() {
private var onSelectAction: ((StorageDevice) -> Unit)? = null
private var mAdapter: StorageSelectArrayAdapter? = null
private var mTitle: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
setStyle(STYLE_NORMAL, StyleUtils.dialogStyle())
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.storage_select_dialog, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
title.text = mTitle
mAdapter = StorageSelectArrayAdapter(
activity!!,
StorageDeviceUtils.getStorageDevices(activity!!, true)
)
device_list.adapter = mAdapter
device_list.onItemClickListener = OnItemClickListener { _, _, position, _ ->
onSelectAction?.invoke(mAdapter!!.getItem(position)!!)
dismiss()
}
}
override fun show(fm: FragmentManager, text: String) {
mTitle = text
super.show(fm, text)
}
fun setOnSelectListener(onSelectAction: (StorageDevice) -> Unit) {
this.onSelectAction = onSelectAction
}
}

View File

@ -30,6 +30,8 @@ import javax.inject.Inject
class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) {
fun books() = box.asFlowable()
.doOnNext(::removeBooksThatDoNotExist)
.map { books -> books.filter { it.file.exists() } }
.map { it.map(::BookOnDisk) }
fun getBooks() = box.all.map(::BookOnDisk)
@ -52,4 +54,12 @@ class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) {
fun migrationInsert(books: ArrayList<Book>) {
insert(books.map { BookOnDisk(book = it, file = it.file) })
}
private fun removeBooksThatDoNotExist(books: MutableList<BookOnDiskEntity>) {
delete(books.filterNot { it.file.exists() })
}
private fun delete(books: List<BookOnDiskEntity>) {
box.remove(books)
}
}

View File

@ -26,15 +26,13 @@ import org.kiwix.kiwixmobile.database.newdb.entities.BookmarkEntity_
import javax.inject.Inject
class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) {
fun getBookmarks(fromCurrentBook: Boolean): List<BookmarkItem> {
return box.query {
if (fromCurrentBook) {
equal(BookmarkEntity_.zimId, ZimContentProvider.getId() ?: "")
}
order(BookmarkEntity_.bookmarkTitle)
}.find()
.map(::BookmarkItem)
}
fun getBookmarks(fromCurrentBook: Boolean) = box.query {
if (fromCurrentBook) {
equal(BookmarkEntity_.zimName, ZimContentProvider.getName() ?: "")
}
order(BookmarkEntity_.bookmarkTitle)
}.find()
.map(::BookmarkItem)
fun getCurrentZimBookmarksUrl() = box.query {
equal(BookmarkEntity_.zimId, ZimContentProvider.getId() ?: "")

View File

@ -43,6 +43,7 @@ import java.io.File;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import kotlin.Unit;
import org.kiwix.kiwixmobile.BuildConfig;
import org.kiwix.kiwixmobile.KiwixApplication;
import org.kiwix.kiwixmobile.R;
@ -50,7 +51,6 @@ import org.kiwix.kiwixmobile.base.BaseActivity;
import org.kiwix.kiwixmobile.main.MainActivity;
import org.kiwix.kiwixmobile.utils.LanguageUtils;
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil;
import org.kiwix.kiwixmobile.utils.StyleUtils;
import org.kiwix.kiwixmobile.zim_manager.library_view.LibraryUtils;
import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_WEBVIEWS_LIST;
@ -81,9 +81,9 @@ public class KiwixSettingsActivity extends BaseActivity {
allHistoryCleared = false;
getFragmentManager()
.beginTransaction().
replace(R.id.content_frame, new PrefsFragment())
.commit();
.beginTransaction().
replace(R.id.content_frame, new PrefsFragment())
.commit();
setUpToolbar();
}
@ -110,8 +110,8 @@ public class KiwixSettingsActivity extends BaseActivity {
}
public static class PrefsFragment extends PreferenceFragment implements
SettingsContract.View,
SharedPreferences.OnSharedPreferenceChangeListener, StorageSelectDialog.OnSelectListener {
SettingsContract.View,
SharedPreferences.OnSharedPreferenceChangeListener {
@Inject
SettingsPresenter presenter;
@ -329,29 +329,25 @@ public class KiwixSettingsActivity extends BaseActivity {
public void openFolderSelect() {
StorageSelectDialog dialogFragment = new StorageSelectDialog();
Bundle b = new Bundle();
b.putString(StorageSelectDialog.STORAGE_DIALOG_INTERNAL,
getResources().getString(R.string.internal_storage));
b.putString(StorageSelectDialog.STORAGE_DIALOG_EXTERNAL,
getResources().getString(R.string.external_storage));
b.putInt(StorageSelectDialog.STORAGE_DIALOG_THEME, StyleUtils.dialogStyle());
dialogFragment.setArguments(b);
dialogFragment.setOnSelectListener(this);
dialogFragment.show(((AppCompatActivity) getActivity()).getSupportFragmentManager(), getResources().getString(R.string.pref_storage));
dialogFragment.setOnSelectListener(storageDevice -> {
selectionCallback(storageDevice);
return Unit.INSTANCE;
});
dialogFragment.show(((AppCompatActivity) getActivity()).getSupportFragmentManager(),
getResources().getString(R.string.pref_storage));
}
@Override
public void selectionCallback(StorageDevice storageDevice) {
findPreference(PREF_STORAGE).setSummary(storageDevice.getSize());
private void selectionCallback(StorageDevice storageDevice) {
findPreference(PREF_STORAGE).setSummary(storageDevice.getAvailableSpace());
sharedPreferenceUtil.putPrefStorage(storageDevice.getName());
if (storageDevice.isInternal()) {
findPreference(PREF_STORAGE).setTitle(getResources().getString(R.string.internal_storage));
sharedPreferenceUtil.putPrefStorageTitle(
getResources().getString(R.string.internal_storage));
getResources().getString(R.string.internal_storage));
} else {
findPreference(PREF_STORAGE).setTitle(getResources().getString(R.string.external_storage));
sharedPreferenceUtil.putPrefStorageTitle(
getResources().getString(R.string.external_storage));
getResources().getString(R.string.external_storage));
}
}
}

View File

@ -17,7 +17,6 @@
*/
package org.kiwix.kiwixmobile.zim_manager.library_view
import android.annotation.SuppressLint
import android.net.ConnectivityManager
import android.os.Bundle
import android.view.LayoutInflater
@ -49,7 +48,6 @@ import org.kiwix.kiwixmobile.utils.DialogShower
import org.kiwix.kiwixmobile.utils.KiwixDialog.YesNoDialog.WifiOnly
import org.kiwix.kiwixmobile.utils.NetworkUtils
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.utils.StyleUtils
import org.kiwix.kiwixmobile.utils.TestingUtils
import org.kiwix.kiwixmobile.zim_manager.NetworkState
import org.kiwix.kiwixmobile.zim_manager.NetworkState.CONNECTED
@ -210,23 +208,9 @@ class LibraryFragment : BaseFragment() {
private fun notEnoughSpaceAvailable(item: BookItem) =
spaceAvailable < item.book.size.toLong() * 1024f
@SuppressLint("ImplicitSamInstance")
private fun showStorageSelectDialog() {
StorageSelectDialog()
.apply {
arguments = Bundle().apply {
putString(
StorageSelectDialog.STORAGE_DIALOG_INTERNAL,
this@LibraryFragment.getString(string.internal_storage)
)
putString(
StorageSelectDialog.STORAGE_DIALOG_EXTERNAL,
this@LibraryFragment.getString(string.external_storage)
)
putInt(StorageSelectDialog.STORAGE_DIALOG_THEME, StyleUtils.dialogStyle())
}
setOnSelectListener(::storeDeviceInPreferences)
}
.show(fragmentManager, getString(string.pref_storage))
}
private fun showStorageSelectDialog() = StorageSelectDialog()
.apply {
setOnSelectListener(::storeDeviceInPreferences)
}
.show(fragmentManager!!, getString(string.pref_storage))
}

View File

@ -1,53 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
>
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:textAlignment="center"
android:textSize="22sp"
/>
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:textAlignment="center"
android:textSize="22sp"
/>
<ListView
android:id="@+id/device_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
</ListView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:inputType="textPersonName"
android:text="@string/slash"
android:importantForAutofill="no"
tools:targetApi="o"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/plus"
/>
</LinearLayout>
android:id="@+id/device_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>

View File

@ -6,6 +6,4 @@
<item>large</item>
</string-array>
<string name="slash">/</string>
<string name="plus">+</string>
</resources>

View File

@ -29,7 +29,7 @@ class LanguageTest {
@Nested
inner class Equals {
@Test
@Suppress("UnusedEquals", "ReplaceCallWithBinaryOperator") //cannot == Unit
@Suppress("UnusedEquals", "ReplaceCallWithBinaryOperator") // cannot == Unit
fun `throws exception when object is not language item`() {
assertThrows(ClassCastException::class.java) { language().equals(Unit) }
}