Merge remote-tracking branch 'origin/develop' into feature/macgills/#1258-leakcanary-ci

# Conflicts:
#	app/build.gradle
This commit is contained in:
Sean Mac Gillicuddy 2019-07-15 09:50:54 +01:00
commit 9f1149b507
12 changed files with 637 additions and 11 deletions

View File

@ -76,6 +76,7 @@ before_deploy:
- export UNIVERSAL_RELEASE_APK=$OUTPUT_DIR/release/*universal*.apk
- export UNIVERSAL_DEBUG_APK=$OUTPUT_DIR/debug/*universal*.apk
- export SSH_KEY=travisci_builder_id_key
- chmod 600 $SSH_KEY
deploy:

View File

@ -56,9 +56,6 @@ dependencies {
archs = file("../kiwixlib/src/main/jniLibs").list()
}
// Storage Devices
implementation "eu.mhutti1.utils.storage:android-storage-devices:0.6.2"
// Android Support
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation "com.google.android.material:material:$materialVersion"
@ -232,9 +229,11 @@ android {
//TODO stop ignoring
ignore 'MissingTranslation',
'CheckResult',
'LabelFor',
'DuplicateStrings',
'LogConditional'
warning 'UnknownNullness'
warning 'UnknownNullness',
'SelectableText'
baseline file("lint-baseline.xml")
}

View File

@ -0,0 +1,59 @@
/*
* 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,164 @@
/*
* 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,131 @@
/*
* 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,72 @@
/*
* 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,114 @@
/*
* 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

@ -19,7 +19,6 @@
package org.kiwix.kiwixmobile.settings;
import android.app.FragmentManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
@ -35,6 +34,7 @@ import android.webkit.WebView;
import android.widget.BaseAdapter;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import eu.mhutti1.utils.storage.StorageDevice;
import eu.mhutti1.utils.storage.StorageSelectDialog;
@ -200,7 +200,7 @@ public class KiwixSettingsActivity extends BaseActivity {
String selectedLang = sharedPreferenceUtil.getPrefLanguage(Locale.getDefault().toString());
List<String> languageCodeList = new LanguageUtils(getActivity()).getKeys();
selectedLang = languageCodeList.contains(selectedLang) ? selectedLang : "en";
String code[] = languageCodeList.toArray(new String[languageCodeList.size()]);
String code[] = languageCodeList.toArray(new String[0]);
String[] entries = new String[code.length];
for (int index = 0; index < code.length; index++) {
Locale locale = new Locale(code[index]);
@ -322,7 +322,6 @@ public class KiwixSettingsActivity extends BaseActivity {
}
public void openFolderSelect() {
FragmentManager fm = getFragmentManager();
StorageSelectDialog dialogFragment = new StorageSelectDialog();
Bundle b = new Bundle();
b.putString(StorageSelectDialog.STORAGE_DIALOG_INTERNAL,
@ -332,7 +331,7 @@ public class KiwixSettingsActivity extends BaseActivity {
b.putInt(StorageSelectDialog.STORAGE_DIALOG_THEME, StyleUtils.dialogStyle());
dialogFragment.setArguments(b);
dialogFragment.setOnSelectListener(this);
dialogFragment.show(fm, getResources().getString(R.string.pref_storage));
dialogFragment.show(((AppCompatActivity) getActivity()).getSupportFragmentManager(), getResources().getString(R.string.pref_storage));
}
@Override

View File

@ -17,6 +17,7 @@
*/
package org.kiwix.kiwixmobile.zim_manager.library_view
import android.annotation.SuppressLint
import android.net.ConnectivityManager
import android.os.Bundle
import android.view.LayoutInflater
@ -30,7 +31,7 @@ import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import eu.mhutti1.utils.storage.StorageDevice
import eu.mhutti1.utils.storage.support.StorageSelectDialog
import eu.mhutti1.utils.storage.StorageSelectDialog
import kotlinx.android.synthetic.main.activity_library.libraryErrorText
import kotlinx.android.synthetic.main.activity_library.libraryList
import kotlinx.android.synthetic.main.activity_library.librarySwipeRefresh
@ -209,8 +210,10 @@ class LibraryFragment : BaseFragment() {
private fun notEnoughSpaceAvailable(item: BookItem) =
spaceAvailable < item.book.size.toLong() * 1024f
@SuppressLint("ImplicitSamInstance")
private fun showStorageSelectDialog() {
StorageSelectDialog().apply {
StorageSelectDialog()
.apply {
arguments = Bundle().apply {
putString(
StorageSelectDialog.STORAGE_DIALOG_INTERNAL,

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
>
<TextView
android:id="@+id/file_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:textSize="18sp"
/>
<TextView
android:id="@+id/file_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_toEndOf="@id/file_name"
android:gravity="end"
android:padding="10dp"
android:textSize="18sp"
android:layout_toRightOf="@id/file_name"
/>
</RelativeLayout>

View File

@ -0,0 +1,53 @@
<?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"
>
<TextView
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>
</LinearLayout>

View File

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