mirror of https://github.com/kurisufriend/Clover
# Conflicts: # Clover/app/build.gradle # Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java # Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java # Clover/app/src/main/java/org/floens/chan/core/model/Post.java # Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java # Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java # Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java # Clover/app/src/main/java/org/floens/chan/test/TestActivity.java # Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java # Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java # Clover/build.gradlemultisite
commit
f337483a02
@ -1,46 +0,0 @@ |
|||||||
/* |
|
||||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
|
||||||
* Copyright (C) 2014 Floens |
|
||||||
* |
|
||||||
* 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 org.floens.chan.core.model; |
|
||||||
|
|
||||||
import org.floens.chan.core.saver.StorageHelper; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
|
|
||||||
public class FileItem { |
|
||||||
public File file; |
|
||||||
|
|
||||||
public FileItem(File file) { |
|
||||||
this.file = file; |
|
||||||
} |
|
||||||
|
|
||||||
public boolean isFile() { |
|
||||||
return file.isFile(); |
|
||||||
} |
|
||||||
|
|
||||||
public boolean isFolder() { |
|
||||||
return file.isDirectory(); |
|
||||||
} |
|
||||||
|
|
||||||
public boolean canNavigate() { |
|
||||||
return StorageHelper.canNavigate(file); |
|
||||||
} |
|
||||||
|
|
||||||
public boolean canOpen() { |
|
||||||
return StorageHelper.canOpen(file); |
|
||||||
} |
|
||||||
} |
|
@ -1,34 +0,0 @@ |
|||||||
/* |
|
||||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
|
||||||
* Copyright (C) 2014 Floens |
|
||||||
* |
|
||||||
* 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 org.floens.chan.core.model; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
import java.util.List; |
|
||||||
|
|
||||||
public class FileItems { |
|
||||||
public final File path; |
|
||||||
public final List<FileItem> fileItems; |
|
||||||
|
|
||||||
public final boolean canNavigateUp; |
|
||||||
|
|
||||||
public FileItems(File path, List<FileItem> fileItems, boolean canNavigateUp) { |
|
||||||
this.path = path; |
|
||||||
this.fileItems = fileItems; |
|
||||||
this.canNavigateUp = canNavigateUp; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,162 @@ |
|||||||
|
/* |
||||||
|
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||||
|
* Copyright (C) 2014 Floens |
||||||
|
* |
||||||
|
* 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 org.floens.chan.core.net; |
||||||
|
|
||||||
|
|
||||||
|
import android.util.JsonReader; |
||||||
|
|
||||||
|
import com.android.volley.Response; |
||||||
|
|
||||||
|
import org.floens.chan.BuildConfig; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.text.DateFormat; |
||||||
|
import java.text.ParseException; |
||||||
|
import java.text.SimpleDateFormat; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Date; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Locale; |
||||||
|
|
||||||
|
import okhttp3.HttpUrl; |
||||||
|
|
||||||
|
public class UpdateApiRequest extends JsonReaderRequest<UpdateApiRequest.UpdateApiResponse> { |
||||||
|
public static final String TYPE_UPDATE = "update"; |
||||||
|
|
||||||
|
private static final int API_VERSION = 1; |
||||||
|
|
||||||
|
private String forFlavor; |
||||||
|
|
||||||
|
public UpdateApiRequest(Response.Listener<UpdateApiResponse> listener, Response.ErrorListener errorListener) { |
||||||
|
super(BuildConfig.UPDATE_API_ENDPOINT, listener, errorListener); |
||||||
|
forFlavor = BuildConfig.FLAVOR; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public UpdateApiResponse readJson(JsonReader reader) throws Exception { |
||||||
|
reader.beginObject(); |
||||||
|
|
||||||
|
UpdateApiResponse response = new UpdateApiResponse(); |
||||||
|
|
||||||
|
int apiVersion; |
||||||
|
out: |
||||||
|
while (reader.hasNext()) { |
||||||
|
switch (reader.nextName()) { |
||||||
|
case "api_version": |
||||||
|
apiVersion = reader.nextInt(); |
||||||
|
|
||||||
|
if (apiVersion > API_VERSION) { |
||||||
|
response.newerApiVersion = true; |
||||||
|
|
||||||
|
while (reader.hasNext()) reader.skipValue(); |
||||||
|
|
||||||
|
break out; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
case "messages": |
||||||
|
reader.beginArray(); |
||||||
|
while (reader.hasNext()) { |
||||||
|
response.messages.add(readMessage(reader)); |
||||||
|
} |
||||||
|
reader.endArray(); |
||||||
|
break; |
||||||
|
case "check_interval": |
||||||
|
response.checkIntervalMs = reader.nextLong(); |
||||||
|
break; |
||||||
|
default: |
||||||
|
reader.skipValue(); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
reader.endObject(); |
||||||
|
|
||||||
|
return response; |
||||||
|
} |
||||||
|
|
||||||
|
private UpdateApiMessage readMessage(JsonReader reader) throws IOException { |
||||||
|
reader.beginObject(); |
||||||
|
|
||||||
|
UpdateApiMessage message = new UpdateApiMessage(); |
||||||
|
|
||||||
|
while (reader.hasNext()) { |
||||||
|
switch (reader.nextName()) { |
||||||
|
case "type": |
||||||
|
message.type = reader.nextString(); |
||||||
|
break; |
||||||
|
case "code": |
||||||
|
message.code = reader.nextInt(); |
||||||
|
break; |
||||||
|
case "date": |
||||||
|
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); |
||||||
|
try { |
||||||
|
message.date = format.parse(reader.nextString()); |
||||||
|
} catch (ParseException ignore) { |
||||||
|
} |
||||||
|
break; |
||||||
|
case "message_html": |
||||||
|
message.messageHtml = reader.nextString(); |
||||||
|
break; |
||||||
|
case "apk": |
||||||
|
reader.beginObject(); |
||||||
|
while (reader.hasNext()) { |
||||||
|
if (reader.nextName().equals(forFlavor)) { |
||||||
|
reader.beginObject(); |
||||||
|
while (reader.hasNext()) { |
||||||
|
switch (reader.nextName()) { |
||||||
|
case "url": |
||||||
|
message.apkUrl = HttpUrl.parse(reader.nextString()); |
||||||
|
break; |
||||||
|
default: |
||||||
|
reader.skipValue(); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
reader.endObject(); |
||||||
|
} else { |
||||||
|
reader.skipValue(); |
||||||
|
} |
||||||
|
} |
||||||
|
reader.endObject(); |
||||||
|
break; |
||||||
|
default: |
||||||
|
reader.skipValue(); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
reader.endObject(); |
||||||
|
|
||||||
|
return message; |
||||||
|
} |
||||||
|
|
||||||
|
public static class UpdateApiResponse { |
||||||
|
public boolean newerApiVersion; |
||||||
|
public List<UpdateApiMessage> messages = new ArrayList<>(); |
||||||
|
public long checkIntervalMs; |
||||||
|
} |
||||||
|
|
||||||
|
public static class UpdateApiMessage { |
||||||
|
public String type; |
||||||
|
public int code; |
||||||
|
public Date date; |
||||||
|
public String messageHtml; |
||||||
|
public HttpUrl apkUrl; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
/* |
||||||
|
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||||
|
* Copyright (C) 2014 Floens |
||||||
|
* |
||||||
|
* 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 org.floens.chan.core.settings; |
||||||
|
|
||||||
|
import android.content.SharedPreferences; |
||||||
|
|
||||||
|
public class LongSetting extends Setting<Long> { |
||||||
|
private boolean hasCached = false; |
||||||
|
private Long cached; |
||||||
|
|
||||||
|
public LongSetting(SharedPreferences sharedPreferences, String key, Long def) { |
||||||
|
super(sharedPreferences, key, def); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Long get() { |
||||||
|
if (hasCached) { |
||||||
|
return cached; |
||||||
|
} else { |
||||||
|
cached = sharedPreferences.getLong(key, def); |
||||||
|
hasCached = true; |
||||||
|
return cached; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void set(Long value) { |
||||||
|
if (!value.equals(get())) { |
||||||
|
sharedPreferences.edit().putLong(key, value).apply(); |
||||||
|
cached = value; |
||||||
|
onValueChanged(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,249 @@ |
|||||||
|
/* |
||||||
|
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||||
|
* Copyright (C) 2014 Floens |
||||||
|
* |
||||||
|
* 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 org.floens.chan.core.update; |
||||||
|
|
||||||
|
|
||||||
|
import android.content.Intent; |
||||||
|
import android.net.Uri; |
||||||
|
import android.os.Environment; |
||||||
|
import android.os.StrictMode; |
||||||
|
import android.text.TextUtils; |
||||||
|
|
||||||
|
import com.android.volley.RequestQueue; |
||||||
|
import com.android.volley.Response; |
||||||
|
import com.android.volley.VolleyError; |
||||||
|
|
||||||
|
import org.floens.chan.BuildConfig; |
||||||
|
import org.floens.chan.core.cache.FileCache; |
||||||
|
import org.floens.chan.core.net.UpdateApiRequest; |
||||||
|
import org.floens.chan.core.settings.ChanSettings; |
||||||
|
import org.floens.chan.utils.IOUtils; |
||||||
|
import org.floens.chan.utils.Logger; |
||||||
|
import org.floens.chan.utils.Time; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
import javax.inject.Inject; |
||||||
|
|
||||||
|
import okhttp3.HttpUrl; |
||||||
|
|
||||||
|
import static org.floens.chan.Chan.getGraph; |
||||||
|
|
||||||
|
/** |
||||||
|
* Calls the update API and downloads and requests installs of APK files. |
||||||
|
* <p>The APK files are downloaded to the public Download directory, and the default APK install |
||||||
|
* screen is launched after downloading. |
||||||
|
*/ |
||||||
|
public class UpdateManager { |
||||||
|
public static final long DEFAULT_UPDATE_CHECK_INTERVAL_MS = 1000 * 60 * 60 * 24 * 5; // 5 days
|
||||||
|
|
||||||
|
private static final String TAG = "UpdateManager"; |
||||||
|
|
||||||
|
private static final String DOWNLOAD_FILE = "Clover_update.apk"; |
||||||
|
|
||||||
|
@Inject |
||||||
|
RequestQueue volleyRequestQueue; |
||||||
|
|
||||||
|
@Inject |
||||||
|
FileCache fileCache; |
||||||
|
|
||||||
|
private UpdateCallback callback; |
||||||
|
|
||||||
|
public UpdateManager(UpdateCallback callback) { |
||||||
|
getGraph().inject(this); |
||||||
|
this.callback = callback; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isUpdatingAvailable() { |
||||||
|
return !TextUtils.isEmpty(BuildConfig.UPDATE_API_ENDPOINT); |
||||||
|
} |
||||||
|
|
||||||
|
public void runUpdateApi(final boolean manual) { |
||||||
|
if (!manual) { |
||||||
|
long lastUpdateTime = ChanSettings.updateCheckTime.get(); |
||||||
|
long interval = ChanSettings.updateCheckInterval.get(); |
||||||
|
long now = Time.get(); |
||||||
|
long delta = (lastUpdateTime + interval) - now; |
||||||
|
if (delta > 0) { |
||||||
|
return; |
||||||
|
} else { |
||||||
|
ChanSettings.updateCheckTime.set(now); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Logger.d(TAG, "Calling update API"); |
||||||
|
volleyRequestQueue.add(new UpdateApiRequest(new Response.Listener<UpdateApiRequest.UpdateApiResponse>() { |
||||||
|
@Override |
||||||
|
public void onResponse(UpdateApiRequest.UpdateApiResponse response) { |
||||||
|
if (!processUpdateApiResponse(response) && manual) { |
||||||
|
callback.onManualCheckNone(); |
||||||
|
} |
||||||
|
} |
||||||
|
}, new Response.ErrorListener() { |
||||||
|
@Override |
||||||
|
public void onErrorResponse(VolleyError error) { |
||||||
|
Logger.e(TAG, "Failed to process API call for updating", error); |
||||||
|
|
||||||
|
if (manual) { |
||||||
|
callback.onManualCheckFailed(); |
||||||
|
} |
||||||
|
} |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean processUpdateApiResponse(UpdateApiRequest.UpdateApiResponse response) { |
||||||
|
if (response.newerApiVersion) { |
||||||
|
Logger.e(TAG, "API endpoint reports a higher API version than we support, aborting update check."); |
||||||
|
|
||||||
|
// ignore
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (response.checkIntervalMs != 0) { |
||||||
|
ChanSettings.updateCheckInterval.set(response.checkIntervalMs); |
||||||
|
} |
||||||
|
|
||||||
|
for (UpdateApiRequest.UpdateApiMessage message : response.messages) { |
||||||
|
if (processUpdateMessage(message)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean processUpdateMessage(UpdateApiRequest.UpdateApiMessage message) { |
||||||
|
if (message.code <= BuildConfig.VERSION_CODE) { |
||||||
|
Logger.d(TAG, "No newer version available (" + BuildConfig.VERSION_CODE + " >= " + message.code + ")."); |
||||||
|
// Our code is newer than the message
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (message.type.equals(UpdateApiRequest.TYPE_UPDATE)) { |
||||||
|
if (message.apkUrl == null) { |
||||||
|
Logger.i(TAG, "Update available but none for this build flavor."); |
||||||
|
// Not for this flavor, discard.
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
Logger.i(TAG, "Update available (" + message.code + ") with url \"" + message.apkUrl + "\"."); |
||||||
|
callback.showUpdateAvailableDialog(message); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Install the APK file specified in {@code update}. This methods needs the storage permission. |
||||||
|
* |
||||||
|
* @param update update with apk details. |
||||||
|
*/ |
||||||
|
public void doUpdate(Update update) { |
||||||
|
fileCache.downloadFile(update.apkUrl.toString(), new FileCache.DownloadedCallback() { |
||||||
|
@Override |
||||||
|
public void onProgress(long downloaded, long total, boolean done) { |
||||||
|
if (!done) callback.onUpdateDownloadProgress(downloaded, total); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onSuccess(File file) { |
||||||
|
callback.onUpdateDownloadSuccess(); |
||||||
|
copyToPublicDirectory(file); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onFail(boolean notFound) { |
||||||
|
callback.onUpdateDownloadFailed(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public void retry(Install install) { |
||||||
|
installApk(install); |
||||||
|
} |
||||||
|
|
||||||
|
private void copyToPublicDirectory(File cacheFile) { |
||||||
|
File out = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), DOWNLOAD_FILE); |
||||||
|
try { |
||||||
|
IOUtils.copyFile(cacheFile, out); |
||||||
|
} catch (IOException e) { |
||||||
|
Logger.e(TAG, "requestApkInstall", e); |
||||||
|
callback.onUpdateDownloadMoveFailed(); |
||||||
|
return; |
||||||
|
} |
||||||
|
installApk(new Install(out)); |
||||||
|
} |
||||||
|
|
||||||
|
private void installApk(Install install) { |
||||||
|
// First open the dialog that asks to retry and calls this method again.
|
||||||
|
callback.openUpdateRetryDialog(install); |
||||||
|
|
||||||
|
// Then launch the APK install intent.
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW); |
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); |
||||||
|
intent.setDataAndType(Uri.fromFile(install.installFile), "application/vnd.android.package-archive"); |
||||||
|
|
||||||
|
// The installer wants a content scheme from android N and up,
|
||||||
|
// but I don't feel like implementing a content provider just for this feature.
|
||||||
|
// Temporary change the strictmode policy while starting the intent.
|
||||||
|
StrictMode.VmPolicy vmPolicy = StrictMode.getVmPolicy(); |
||||||
|
StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX); |
||||||
|
|
||||||
|
callback.onUpdateOpenInstallScreen(intent); |
||||||
|
|
||||||
|
StrictMode.setVmPolicy(vmPolicy); |
||||||
|
} |
||||||
|
|
||||||
|
public static class Update { |
||||||
|
private HttpUrl apkUrl; |
||||||
|
|
||||||
|
public Update(HttpUrl apkUrl) { |
||||||
|
this.apkUrl = apkUrl; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static class Install { |
||||||
|
private File installFile; |
||||||
|
|
||||||
|
public Install(File installFile) { |
||||||
|
this.installFile = installFile; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public interface UpdateCallback { |
||||||
|
void onManualCheckNone(); |
||||||
|
|
||||||
|
void onManualCheckFailed(); |
||||||
|
|
||||||
|
void showUpdateAvailableDialog(UpdateApiRequest.UpdateApiMessage message); |
||||||
|
|
||||||
|
void onUpdateDownloadProgress(long downloaded, long total); |
||||||
|
|
||||||
|
void onUpdateDownloadSuccess(); |
||||||
|
|
||||||
|
void onUpdateDownloadFailed(); |
||||||
|
|
||||||
|
void onUpdateDownloadMoveFailed(); |
||||||
|
|
||||||
|
void onUpdateOpenInstallScreen(Intent intent); |
||||||
|
|
||||||
|
void openUpdateRetryDialog(Install install); |
||||||
|
} |
||||||
|
} |
@ -1,303 +0,0 @@ |
|||||||
/* |
|
||||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
|
||||||
* Copyright (C) 2014 Floens |
|
||||||
* |
|
||||||
* 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 org.floens.chan.test; |
|
||||||
|
|
||||||
import android.app.Activity; |
|
||||||
import android.os.Bundle; |
|
||||||
import android.os.Handler; |
|
||||||
import android.os.Looper; |
|
||||||
import android.view.View; |
|
||||||
import android.widget.Button; |
|
||||||
import android.widget.LinearLayout; |
|
||||||
|
|
||||||
import org.floens.chan.Chan; |
|
||||||
import org.floens.chan.chan.ChanLoader; |
|
||||||
import org.floens.chan.core.cache.FileCache; |
|
||||||
import org.floens.chan.core.exception.ChanLoaderException; |
|
||||||
import org.floens.chan.core.model.ChanThread; |
|
||||||
import org.floens.chan.core.model.Loadable; |
|
||||||
import org.floens.chan.core.model.Post; |
|
||||||
import org.floens.chan.ui.theme.ThemeHelper; |
|
||||||
import org.floens.chan.utils.Logger; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
|
|
||||||
// Poor mans unit testing.
|
|
||||||
// Move to proper unit testing when the gradle plugin fully supports it.
|
|
||||||
public class TestActivity extends Activity implements View.OnClickListener { |
|
||||||
private static final String TAG = "FileCacheTest"; |
|
||||||
private final Handler handler = new Handler(Looper.getMainLooper()); |
|
||||||
|
|
||||||
private Button clearCache; |
|
||||||
private Button stats; |
|
||||||
private Button simpleTest; |
|
||||||
private Button cacheTest; |
|
||||||
private Button timeoutTest; |
|
||||||
|
|
||||||
private FileCache fileCache; |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onCreate(Bundle savedInstanceState) { |
|
||||||
super.onCreate(savedInstanceState); |
|
||||||
|
|
||||||
ThemeHelper.getInstance().setupContext(this); |
|
||||||
|
|
||||||
LinearLayout linearLayout = new LinearLayout(this); |
|
||||||
linearLayout.setOrientation(LinearLayout.VERTICAL); |
|
||||||
|
|
||||||
clearCache = new Button(this); |
|
||||||
clearCache.setText("Clear cache"); |
|
||||||
clearCache.setOnClickListener(this); |
|
||||||
linearLayout.addView(clearCache); |
|
||||||
|
|
||||||
stats = new Button(this); |
|
||||||
stats.setText("Stats"); |
|
||||||
stats.setOnClickListener(this); |
|
||||||
linearLayout.addView(stats); |
|
||||||
|
|
||||||
simpleTest = new Button(this); |
|
||||||
simpleTest.setText("Test download and cancel"); |
|
||||||
simpleTest.setOnClickListener(this); |
|
||||||
linearLayout.addView(simpleTest); |
|
||||||
|
|
||||||
cacheTest = new Button(this); |
|
||||||
cacheTest.setText("Test cache size"); |
|
||||||
cacheTest.setOnClickListener(this); |
|
||||||
linearLayout.addView(cacheTest); |
|
||||||
|
|
||||||
timeoutTest = new Button(this); |
|
||||||
timeoutTest.setText("Test multiple parallel"); |
|
||||||
timeoutTest.setOnClickListener(this); |
|
||||||
linearLayout.addView(timeoutTest); |
|
||||||
|
|
||||||
setContentView(linearLayout); |
|
||||||
|
|
||||||
File cacheDir = getExternalCacheDir() != null ? getExternalCacheDir() : getCacheDir(); |
|
||||||
File fileCacheDir = new File(cacheDir, "filecache"); |
|
||||||
fileCache = new FileCache(fileCacheDir, 50 * 1024 * 1024, Chan.getInstance().getUserAgent()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onClick(View v) { |
|
||||||
if (v == clearCache) { |
|
||||||
clearCache(); |
|
||||||
} else if (v == stats) { |
|
||||||
stats(); |
|
||||||
} else if (v == simpleTest) { |
|
||||||
testDownloadAndCancel(); |
|
||||||
} else if (v == cacheTest) { |
|
||||||
testCache(); |
|
||||||
} else if (v == timeoutTest) { |
|
||||||
testTimeout(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public void clearCache() { |
|
||||||
fileCache.clearCache(); |
|
||||||
} |
|
||||||
|
|
||||||
public void stats() { |
|
||||||
fileCache.logStats(); |
|
||||||
} |
|
||||||
|
|
||||||
public void testDownloadAndCancel() { |
|
||||||
// 1.9MB file of the clover Logger.i(TAG,
|
|
||||||
final String testImage = "http://a.pomf.se/ndbolc.png"; |
|
||||||
final File cacheFile = fileCache.get(testImage); |
|
||||||
|
|
||||||
Logger.i(TAG, "Downloading " + testImage); |
|
||||||
final FileCache.FileCacheDownloader downloader = fileCache.downloadFile(testImage, new FileCache.DownloadedCallback() { |
|
||||||
@Override |
|
||||||
public void onProgress(long downloaded, long total, boolean done) { |
|
||||||
Logger.i(TAG, "onProgress " + downloaded + "/" + total + " " + done); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onSuccess(File file) { |
|
||||||
Logger.i(TAG, "onSuccess " + file.exists()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFail(boolean notFound) { |
|
||||||
Logger.i(TAG, "onFail Cachefile exists() = " + cacheFile.exists()); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
fileCache.downloadFile(testImage, new FileCache.DownloadedCallback() { |
|
||||||
@Override |
|
||||||
public void onProgress(long downloaded, long total, boolean done) { |
|
||||||
Logger.i(TAG, "2nd progress " + downloaded + "/" + total); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onSuccess(File file) { |
|
||||||
Logger.i(TAG, "2nd onSuccess " + file.exists()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFail(boolean notFound) { |
|
||||||
Logger.i(TAG, "2nd onFail Cachefile exists() = " + cacheFile.exists()); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
}, 200); |
|
||||||
|
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
Logger.i(TAG, "Cancelling download!"); |
|
||||||
downloader.cancel(); |
|
||||||
} |
|
||||||
}, 500); |
|
||||||
|
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
Logger.i(TAG, "File exists() = " + cacheFile.exists()); |
|
||||||
} |
|
||||||
}, 600); |
|
||||||
|
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
final File cache404File = fileCache.get(testImage + "404"); |
|
||||||
fileCache.downloadFile(testImage + "404", new FileCache.DownloadedCallback() { |
|
||||||
@Override |
|
||||||
public void onProgress(long downloaded, long total, boolean done) { |
|
||||||
Logger.i(TAG, "404 progress " + downloaded + "/" + total + " " + done); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onSuccess(File file) { |
|
||||||
Logger.i(TAG, "404 onSuccess " + file.exists()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFail(boolean notFound) { |
|
||||||
Logger.i(TAG, "404 onFail " + cache404File.exists()); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
}, 1000); |
|
||||||
} |
|
||||||
|
|
||||||
private void testCache() { |
|
||||||
// Loadable loadable = Loadable.forCatalog(Sites.defaultSite().boards().boards.get(0));
|
|
||||||
Loadable loadable = null; |
|
||||||
ChanLoader loader = new ChanLoader(loadable); |
|
||||||
loader.addListener(new ChanLoader.ChanLoaderCallback() { |
|
||||||
@Override |
|
||||||
public void onChanLoaderData(ChanThread result) { |
|
||||||
for (Post post : result.posts) { |
|
||||||
if (post.image != null) { |
|
||||||
final String imageUrl = post.image.imageUrl.toString(); |
|
||||||
fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() { |
|
||||||
@Override |
|
||||||
public void onProgress(long downloaded, long total, boolean done) { |
|
||||||
Logger.i(TAG, "Progress for " + imageUrl + " " + downloaded + "/" + total + " " + done); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onSuccess(File file) { |
|
||||||
Logger.i(TAG, "onSuccess for " + imageUrl + " exists() = " + file.exists()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFail(boolean notFound) { |
|
||||||
Logger.i(TAG, "onFail for " + imageUrl); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onChanLoaderError(ChanLoaderException error) { |
|
||||||
|
|
||||||
} |
|
||||||
}); |
|
||||||
loader.requestData(); |
|
||||||
} |
|
||||||
|
|
||||||
private void testTimeout() { |
|
||||||
testTimeoutInner("https://i.4cdn.org/hr/1429923649068.jpg", fileCache, 0); |
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
testTimeoutInner("https://i.4cdn.org/hr/1430058524427.jpg", fileCache, 0); |
|
||||||
} |
|
||||||
}, 200); |
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
testTimeoutInner("https://i.4cdn.org/hr/1430058627352.jpg", fileCache, 0); |
|
||||||
} |
|
||||||
}, 400); |
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
testTimeoutInner("https://i.4cdn.org/hr/1430058580015.jpg", fileCache, 0); |
|
||||||
} |
|
||||||
}, 600); |
|
||||||
} |
|
||||||
|
|
||||||
private void testTimeoutInner(final String url, final FileCache fileCache, final int tries) { |
|
||||||
final File cacheFile = fileCache.get(url); |
|
||||||
Logger.i(TAG, "Downloading " + url + " try " + tries); |
|
||||||
final FileCache.FileCacheDownloader downloader = fileCache.downloadFile(url, new FileCache.DownloadedCallback() { |
|
||||||
@Override |
|
||||||
public void onProgress(long downloaded, long total, boolean done) { |
|
||||||
Logger.i(TAG, "onProgress " + url + " " + downloaded + "/" + total); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onSuccess(File file) { |
|
||||||
Logger.i(TAG, "onSuccess " + file.exists()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFail(boolean notFound) { |
|
||||||
Logger.i(TAG, "onFail Cachefile exists() = " + cacheFile.exists()); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
if (downloader == null) { |
|
||||||
Logger.i(TAG, "Downloader null, cannot cancel"); |
|
||||||
} else { |
|
||||||
downloader.cancel(); |
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
if (tries < 10) { |
|
||||||
testTimeoutInner(url, fileCache, tries + 1); |
|
||||||
} else { |
|
||||||
fileCache.logStats(); |
|
||||||
} |
|
||||||
} |
|
||||||
}, 500); |
|
||||||
} |
|
||||||
} |
|
||||||
}, 1000); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,107 @@ |
|||||||
|
/* |
||||||
|
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||||
|
* Copyright (C) 2014 Floens |
||||||
|
* |
||||||
|
* 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 org.floens.chan.ui.controller; |
||||||
|
|
||||||
|
import android.content.ClipData; |
||||||
|
import android.content.ClipboardManager; |
||||||
|
import android.content.Context; |
||||||
|
import android.view.ViewGroup; |
||||||
|
import android.widget.ScrollView; |
||||||
|
import android.widget.TextView; |
||||||
|
import android.widget.Toast; |
||||||
|
|
||||||
|
import org.floens.chan.R; |
||||||
|
import org.floens.chan.controller.Controller; |
||||||
|
import org.floens.chan.ui.toolbar.ToolbarMenu; |
||||||
|
import org.floens.chan.ui.toolbar.ToolbarMenuItem; |
||||||
|
import org.floens.chan.ui.view.FloatingMenuItem; |
||||||
|
import org.floens.chan.utils.AndroidUtils; |
||||||
|
import org.floens.chan.utils.IOUtils; |
||||||
|
import org.floens.chan.utils.Logger; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import static org.floens.chan.utils.AndroidUtils.getAttrColor; |
||||||
|
|
||||||
|
public class LogsController extends Controller implements ToolbarMenuItem.ToolbarMenuItemCallback { |
||||||
|
private static final String TAG = "LogsController"; |
||||||
|
|
||||||
|
private static final int COPY_ID = 101; |
||||||
|
|
||||||
|
private TextView logTextView; |
||||||
|
|
||||||
|
private String logText; |
||||||
|
|
||||||
|
public LogsController(Context context) { |
||||||
|
super(context); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onCreate() { |
||||||
|
super.onCreate(); |
||||||
|
|
||||||
|
navigationItem.setTitle(org.floens.chan.R.string.settings_logs_screen); |
||||||
|
|
||||||
|
navigationItem.menu = new ToolbarMenu(context); |
||||||
|
List<FloatingMenuItem> items = new ArrayList<>(); |
||||||
|
items.add(new FloatingMenuItem(COPY_ID, R.string.settings_logs_copy)); |
||||||
|
navigationItem.createOverflow(context, this, items); |
||||||
|
|
||||||
|
ScrollView container = new ScrollView(context); |
||||||
|
container.setBackgroundColor(getAttrColor(context, org.floens.chan.R.attr.backcolor)); |
||||||
|
logTextView = new TextView(context); |
||||||
|
container.addView(logTextView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); |
||||||
|
|
||||||
|
view = container; |
||||||
|
|
||||||
|
loadLogs(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onMenuItemClicked(ToolbarMenuItem item) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) { |
||||||
|
if ((int) item.getId() == COPY_ID) { |
||||||
|
ClipboardManager clipboard = (ClipboardManager) AndroidUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); |
||||||
|
ClipData clip = ClipData.newPlainText("Logs", logText); |
||||||
|
clipboard.setPrimaryClip(clip); |
||||||
|
Toast.makeText(context, R.string.settings_logs_copied_to_clipboard, Toast.LENGTH_SHORT).show(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void loadLogs() { |
||||||
|
Process process; |
||||||
|
try { |
||||||
|
process = new ProcessBuilder() |
||||||
|
.command("logcat", "-d", "-v", "tag") |
||||||
|
.start(); |
||||||
|
} catch (IOException e) { |
||||||
|
Logger.e(TAG, "Error starting logcat", e); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
InputStream outputStream = process.getInputStream(); |
||||||
|
logText = IOUtils.readString(outputStream); |
||||||
|
logTextView.setText(logText); |
||||||
|
} |
||||||
|
} |
@ -1,118 +0,0 @@ |
|||||||
/* |
|
||||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
|
||||||
* Copyright (C) 2014 Floens |
|
||||||
* |
|
||||||
* 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 org.floens.chan.ui.helper; |
|
||||||
|
|
||||||
import android.content.Context; |
|
||||||
import android.content.DialogInterface; |
|
||||||
import android.support.v7.app.AlertDialog; |
|
||||||
import android.text.Html; |
|
||||||
import android.widget.Button; |
|
||||||
|
|
||||||
import org.floens.chan.R; |
|
||||||
import org.floens.chan.core.settings.ChanSettings; |
|
||||||
import org.floens.chan.utils.AndroidUtils; |
|
||||||
import org.floens.chan.utils.Logger; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
|
|
||||||
public class PreviousVersionHandler { |
|
||||||
private static final String TAG = "PreviousVersionHandler"; |
|
||||||
|
|
||||||
/* |
|
||||||
* Manifest version code, manifest version name, this version mapping: |
|
||||||
* |
|
||||||
* 28 = v1.1.2 |
|
||||||
* 32 = v1.1.3 |
|
||||||
* 36 = v1.2.0 |
|
||||||
* 39 = v1.2.1 |
|
||||||
* 40 = v1.2.2 |
|
||||||
* 41 = v1.2.3 |
|
||||||
* 42 = v1.2.4 |
|
||||||
* 43 = v1.2.5 |
|
||||||
* 44 = v1.2.6 |
|
||||||
* 46 = v1.2.7 |
|
||||||
* 47 = v1.2.8 |
|
||||||
* 48 = v1.2.9 |
|
||||||
* 49 = v1.2.10 |
|
||||||
* 50 = v1.2.11 |
|
||||||
* 51 = v2.0.0 = 1 |
|
||||||
* 52 = v2.1.0 = 2 |
|
||||||
* 53 = v2.1.1 = 2 |
|
||||||
* 54 = v2.1.2 = 2 |
|
||||||
* 55 = v2.1.3 = 2 |
|
||||||
* 56 = v2.2.0 = 3 |
|
||||||
*/ |
|
||||||
private static final int CURRENT_VERSION = 3; |
|
||||||
|
|
||||||
public void run(Context context) { |
|
||||||
int previous = ChanSettings.previousVersion.get(); |
|
||||||
if (previous < CURRENT_VERSION) { |
|
||||||
if (previous < 1) { |
|
||||||
cleanupOutdatedIonFolder(context); |
|
||||||
} |
|
||||||
|
|
||||||
// Add more previous version checks here
|
|
||||||
|
|
||||||
showMessage(context, CURRENT_VERSION); |
|
||||||
|
|
||||||
ChanSettings.previousVersion.set(CURRENT_VERSION); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private void showMessage(Context context, int version) { |
|
||||||
int resource = context.getResources().getIdentifier("previous_version_" + version, "string", context.getPackageName()); |
|
||||||
if (resource != 0) { |
|
||||||
CharSequence message = Html.fromHtml(context.getString(resource)); |
|
||||||
|
|
||||||
final AlertDialog dialog = new AlertDialog.Builder(context) |
|
||||||
.setMessage(message) |
|
||||||
.setPositiveButton(R.string.ok, null) |
|
||||||
.create(); |
|
||||||
dialog.show(); |
|
||||||
dialog.setCanceledOnTouchOutside(false); |
|
||||||
|
|
||||||
final Button button = dialog.getButton(DialogInterface.BUTTON_POSITIVE); |
|
||||||
button.setEnabled(false); |
|
||||||
AndroidUtils.runOnUiThread(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
dialog.setCanceledOnTouchOutside(true); |
|
||||||
button.setEnabled(true); |
|
||||||
} |
|
||||||
}, 1500); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private void cleanupOutdatedIonFolder(Context context) { |
|
||||||
Logger.i(TAG, "Cleaning up old ion folder"); |
|
||||||
File ionCacheFolder = new File(context.getCacheDir() + "/ion"); |
|
||||||
if (ionCacheFolder.exists() && ionCacheFolder.isDirectory()) { |
|
||||||
Logger.i(TAG, "Clearing old ion folder"); |
|
||||||
for (File file : ionCacheFolder.listFiles()) { |
|
||||||
if (!file.delete()) { |
|
||||||
Logger.i(TAG, "Could not delete old ion file " + file.getName()); |
|
||||||
} |
|
||||||
} |
|
||||||
if (!ionCacheFolder.delete()) { |
|
||||||
Logger.i(TAG, "Could not delete old ion folder"); |
|
||||||
} else { |
|
||||||
Logger.i(TAG, "Deleted old ion folder"); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,286 @@ |
|||||||
|
/* |
||||||
|
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||||
|
* Copyright (C) 2014 Floens |
||||||
|
* |
||||||
|
* 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 org.floens.chan.ui.helper; |
||||||
|
|
||||||
|
import android.Manifest; |
||||||
|
import android.app.ProgressDialog; |
||||||
|
import android.content.Context; |
||||||
|
import android.content.DialogInterface; |
||||||
|
import android.content.Intent; |
||||||
|
import android.support.v7.app.AlertDialog; |
||||||
|
import android.text.Html; |
||||||
|
import android.text.Spanned; |
||||||
|
import android.widget.Button; |
||||||
|
|
||||||
|
import org.floens.chan.BuildConfig; |
||||||
|
import org.floens.chan.R; |
||||||
|
import org.floens.chan.core.net.UpdateApiRequest; |
||||||
|
import org.floens.chan.core.settings.ChanSettings; |
||||||
|
import org.floens.chan.core.update.UpdateManager; |
||||||
|
import org.floens.chan.utils.AndroidUtils; |
||||||
|
import org.floens.chan.utils.Logger; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
|
||||||
|
public class VersionHandler implements UpdateManager.UpdateCallback { |
||||||
|
private static final String TAG = "VersionHandler"; |
||||||
|
|
||||||
|
/* |
||||||
|
* Manifest version code, manifest version name, this version mapping: |
||||||
|
* |
||||||
|
* 28 = v1.1.2 |
||||||
|
* 32 = v1.1.3 |
||||||
|
* 36 = v1.2.0 |
||||||
|
* 39 = v1.2.1 |
||||||
|
* 40 = v1.2.2 |
||||||
|
* 41 = v1.2.3 |
||||||
|
* 42 = v1.2.4 |
||||||
|
* 43 = v1.2.5 |
||||||
|
* 44 = v1.2.6 |
||||||
|
* 46 = v1.2.7 |
||||||
|
* 47 = v1.2.8 |
||||||
|
* 48 = v1.2.9 |
||||||
|
* 49 = v1.2.10 |
||||||
|
* 50 = v1.2.11 |
||||||
|
* 51 = v2.0.0 = 1 |
||||||
|
* 52 = v2.1.0 = 2 |
||||||
|
* 53 = v2.1.1 = 2 |
||||||
|
* 54 = v2.1.2 = 2 |
||||||
|
* 55 = v2.1.3 = 2 |
||||||
|
* 56 = v2.2.0 = 3 |
||||||
|
* Since v2.3.0, this has been aligned with the versionCode as defined in build.gradle |
||||||
|
* It is of the format XXYYZZ, where XX is major, YY is minor, ZZ is patch. |
||||||
|
* 20300 = v2.3.0 = 20300 |
||||||
|
*/ |
||||||
|
private static final int CURRENT_VERSION = BuildConfig.VERSION_CODE; |
||||||
|
|
||||||
|
/** |
||||||
|
* Context to show dialogs to. |
||||||
|
*/ |
||||||
|
private Context context; |
||||||
|
private RuntimePermissionsHelper runtimePermissionsHelper; |
||||||
|
|
||||||
|
private UpdateManager updateManager; |
||||||
|
|
||||||
|
private ProgressDialog updateDownloadDialog; |
||||||
|
|
||||||
|
public VersionHandler(Context context, RuntimePermissionsHelper runtimePermissionsHelper) { |
||||||
|
this.context = context; |
||||||
|
this.runtimePermissionsHelper = runtimePermissionsHelper; |
||||||
|
|
||||||
|
updateManager = new UpdateManager(this); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Runs every time onCreate is called on the StartActivity. |
||||||
|
*/ |
||||||
|
public void run() { |
||||||
|
int previous = ChanSettings.previousVersion.get(); |
||||||
|
if (previous < CURRENT_VERSION) { |
||||||
|
if (previous < 1) { |
||||||
|
cleanupOutdatedIonFolder(context); |
||||||
|
} |
||||||
|
|
||||||
|
// Add more previous version checks here
|
||||||
|
|
||||||
|
showMessage(CURRENT_VERSION); |
||||||
|
|
||||||
|
ChanSettings.previousVersion.set(CURRENT_VERSION); |
||||||
|
|
||||||
|
// Don't process the updater because a dialog is now already showing.
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (updateManager.isUpdatingAvailable()) { |
||||||
|
updateManager.runUpdateApi(false); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isUpdatingAvailable() { |
||||||
|
return updateManager.isUpdatingAvailable(); |
||||||
|
} |
||||||
|
|
||||||
|
public void manualUpdateCheck() { |
||||||
|
updateManager.runUpdateApi(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onManualCheckNone() { |
||||||
|
new AlertDialog.Builder(context) |
||||||
|
.setTitle(R.string.update_none) |
||||||
|
.setPositiveButton(R.string.ok, null) |
||||||
|
.show(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onManualCheckFailed() { |
||||||
|
new AlertDialog.Builder(context) |
||||||
|
.setTitle(R.string.update_check_failed) |
||||||
|
.setPositiveButton(R.string.ok, null) |
||||||
|
.show(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void showUpdateAvailableDialog(final UpdateApiRequest.UpdateApiMessage message) { |
||||||
|
Spanned text = Html.fromHtml(message.messageHtml); |
||||||
|
|
||||||
|
final AlertDialog dialog = new AlertDialog.Builder(context) |
||||||
|
.setMessage(text) |
||||||
|
.setNegativeButton(R.string.update_later, new DialogInterface.OnClickListener() { |
||||||
|
@Override |
||||||
|
public void onClick(DialogInterface dialog, int which) { |
||||||
|
updatePostponed(message); |
||||||
|
} |
||||||
|
}) |
||||||
|
.setPositiveButton(R.string.update_install, new DialogInterface.OnClickListener() { |
||||||
|
@Override |
||||||
|
public void onClick(DialogInterface dialog, int which) { |
||||||
|
updateInstallRequested(message); |
||||||
|
} |
||||||
|
}) |
||||||
|
.create(); |
||||||
|
dialog.show(); |
||||||
|
dialog.setCanceledOnTouchOutside(false); |
||||||
|
} |
||||||
|
|
||||||
|
private void updatePostponed(UpdateApiRequest.UpdateApiMessage message) { |
||||||
|
} |
||||||
|
|
||||||
|
private void updateInstallRequested(final UpdateApiRequest.UpdateApiMessage message) { |
||||||
|
runtimePermissionsHelper.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, new RuntimePermissionsHelper.Callback() { |
||||||
|
@Override |
||||||
|
public void onRuntimePermissionResult(boolean granted) { |
||||||
|
if (granted) { |
||||||
|
createDownloadProgressDialog(); |
||||||
|
updateManager.doUpdate(new UpdateManager.Update(message.apkUrl)); |
||||||
|
} else { |
||||||
|
runtimePermissionsHelper.showPermissionRequiredDialog(context, |
||||||
|
context.getString(R.string.update_storage_permission_required_title), |
||||||
|
context.getString(R.string.update_storage_permission_required), |
||||||
|
new RuntimePermissionsHelper.PermissionRequiredDialogCallback() { |
||||||
|
@Override |
||||||
|
public void retryPermissionRequest() { |
||||||
|
updateInstallRequested(message); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void createDownloadProgressDialog() { |
||||||
|
updateDownloadDialog = new ProgressDialog(context); |
||||||
|
updateDownloadDialog.setCancelable(false); |
||||||
|
updateDownloadDialog.setTitle(R.string.update_install_downloading); |
||||||
|
updateDownloadDialog.setMax(10000); |
||||||
|
updateDownloadDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); |
||||||
|
updateDownloadDialog.setProgressNumberFormat(""); |
||||||
|
updateDownloadDialog.show(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onUpdateDownloadProgress(long downloaded, long total) { |
||||||
|
updateDownloadDialog.setProgress((int) (updateDownloadDialog.getMax() * (downloaded / (double) total))); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onUpdateDownloadSuccess() { |
||||||
|
updateDownloadDialog.dismiss(); |
||||||
|
updateDownloadDialog = null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onUpdateDownloadFailed() { |
||||||
|
updateDownloadDialog.dismiss(); |
||||||
|
updateDownloadDialog = null; |
||||||
|
new AlertDialog.Builder(context) |
||||||
|
.setTitle(R.string.update_install_download_failed) |
||||||
|
.setPositiveButton(R.string.ok, null) |
||||||
|
.show(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onUpdateDownloadMoveFailed() { |
||||||
|
new AlertDialog.Builder(context) |
||||||
|
.setTitle(R.string.update_install_download_move_failed) |
||||||
|
.setPositiveButton(R.string.ok, null) |
||||||
|
.show(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onUpdateOpenInstallScreen(Intent intent) { |
||||||
|
AndroidUtils.openIntent(intent); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void openUpdateRetryDialog(final UpdateManager.Install install) { |
||||||
|
new AlertDialog.Builder(context) |
||||||
|
.setTitle(R.string.update_retry_title) |
||||||
|
.setMessage(R.string.update_retry) |
||||||
|
.setNegativeButton(R.string.cancel, null) |
||||||
|
.setPositiveButton(R.string.update_retry_button, new DialogInterface.OnClickListener() { |
||||||
|
@Override |
||||||
|
public void onClick(DialogInterface dialog, int which) { |
||||||
|
updateManager.retry(install); |
||||||
|
} |
||||||
|
}) |
||||||
|
.show(); |
||||||
|
} |
||||||
|
|
||||||
|
private void showMessage(int version) { |
||||||
|
int resource = context.getResources().getIdentifier("changelog_" + version, "string", context.getPackageName()); |
||||||
|
if (resource != 0) { |
||||||
|
CharSequence message = Html.fromHtml(context.getString(resource)); |
||||||
|
|
||||||
|
final AlertDialog dialog = new AlertDialog.Builder(context) |
||||||
|
.setMessage(message) |
||||||
|
.setPositiveButton(R.string.ok, null) |
||||||
|
.create(); |
||||||
|
dialog.show(); |
||||||
|
dialog.setCanceledOnTouchOutside(false); |
||||||
|
|
||||||
|
final Button button = dialog.getButton(DialogInterface.BUTTON_POSITIVE); |
||||||
|
button.setEnabled(false); |
||||||
|
AndroidUtils.runOnUiThread(new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
dialog.setCanceledOnTouchOutside(true); |
||||||
|
button.setEnabled(true); |
||||||
|
} |
||||||
|
}, 1500); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void cleanupOutdatedIonFolder(Context context) { |
||||||
|
Logger.i(TAG, "Cleaning up old ion folder"); |
||||||
|
File ionCacheFolder = new File(context.getCacheDir() + "/ion"); |
||||||
|
if (ionCacheFolder.exists() && ionCacheFolder.isDirectory()) { |
||||||
|
Logger.i(TAG, "Clearing old ion folder"); |
||||||
|
for (File file : ionCacheFolder.listFiles()) { |
||||||
|
if (!file.delete()) { |
||||||
|
Logger.i(TAG, "Could not delete old ion file " + file.getName()); |
||||||
|
} |
||||||
|
} |
||||||
|
if (!ionCacheFolder.delete()) { |
||||||
|
Logger.i(TAG, "Could not delete old ion folder"); |
||||||
|
} else { |
||||||
|
Logger.i(TAG, "Deleted old ion folder"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
@ -1,6 +1,6 @@ |
|||||||
#Tue Aug 16 12:22:14 CEST 2016 |
#Tue Mar 07 00:32:11 EST 2017 |
||||||
distributionBase=GRADLE_USER_HOME |
distributionBase=GRADLE_USER_HOME |
||||||
distributionPath=wrapper/dists |
distributionPath=wrapper/dists |
||||||
zipStoreBase=GRADLE_USER_HOME |
zipStoreBase=GRADLE_USER_HOME |
||||||
zipStorePath=wrapper/dists |
zipStorePath=wrapper/dists |
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip |
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip |
||||||
|
@ -1,46 +0,0 @@ |
|||||||
mode=checkcaptcha only works on some boards! |
|
||||||
Until a proper api exists for checking what boards have this option, it will not be implemented. |
|
||||||
(that is, a proper way to check if "var preupload_captcha = true" was set) |
|
||||||
|
|
||||||
|
|
||||||
Send to the normal url "https://sys.4chan.org/" + board + "/post"; |
|
||||||
Used in the extension for checking if the captcha is correct when submitting a large file |
|
||||||
|
|
||||||
POST: |
|
||||||
mode=checkcaptcha |
|
||||||
challenge=<normal captcha challenge> |
|
||||||
response=<user response> |
|
||||||
|
|
||||||
|
|
||||||
returns json: |
|
||||||
{ |
|
||||||
"token": "<captcha token>", |
|
||||||
"error": "<error shown to the user>", |
|
||||||
"fail": "<fail logged to console>" |
|
||||||
} |
|
||||||
|
|
||||||
In the extension: |
|
||||||
|
|
||||||
if (response.token) { |
|
||||||
a = $.id("qrCapToken"), |
|
||||||
a.value = response.token, |
|
||||||
a.removeAttribute("disabled"), |
|
||||||
QR.submitDirect() |
|
||||||
} else { |
|
||||||
if (response.error) { |
|
||||||
QR.reloadCaptcha(), |
|
||||||
QR.btn.value = "Post", |
|
||||||
QR.showPostError(response.error)) |
|
||||||
} else { |
|
||||||
if (response.fail) { |
|
||||||
console.log(b.fail), |
|
||||||
QR.submitDirect() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
qrCapToken is a hidden form field with the name captcha_token |
|
||||||
So when token exists in the json, send a normal reply with captcha_token |
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@ |
|||||||
|
{ |
||||||
|
"api_version": 1, |
||||||
|
"messages": [ |
||||||
|
{ |
||||||
|
"type": "update", |
||||||
|
|
||||||
|
"code": 56, |
||||||
|
"date": "2017-03-18T13:23:06.614104", |
||||||
|
"message_html": "<h2>Clover v2.2.0 is available</h2>A new version of Clover is available.<br><br>This release fixes stuff.<br>- aaa<br>- bbb", |
||||||
|
|
||||||
|
"apk": { |
||||||
|
"default": { |
||||||
|
"url": "https://github.com/Floens/Clover/releases/download/v2.2.0/Clover_v2.2.0.apk" |
||||||
|
}, |
||||||
|
"fdroid": { |
||||||
|
"url": "https://f-droid.org/repo/org.floens.chan_56.apk" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
], |
||||||
|
"check_interval": 432000000 |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
update_api.json describes the update check api that Clover loads periodically. |
||||||
|
|
||||||
|
api_version |
||||||
|
Version of this api, always 1. |
||||||
|
|
||||||
|
check_interval |
||||||
|
the interval of loading the file, overrides the default interval of 5 days if set. |
||||||
|
|
||||||
|
messages |
||||||
|
array of messages |
||||||
|
|
||||||
|
type: |
||||||
|
type of the message, only "update" is supported |
||||||
|
|
||||||
|
code: |
||||||
|
code of the new version. if this is higher than the code of the calling app then the message will be processed. |
||||||
|
|
||||||
|
date: |
||||||
|
ISO8601 date, parsed but not used for now. |
||||||
|
|
||||||
|
message_html: |
||||||
|
message shown to the user, parsed with Html.fromHtml() |
||||||
|
|
||||||
|
apk: |
||||||
|
set of apks for each flavor. each key is only parsed if it equals to the flavor name that the app is compiled for. |
||||||
|
|
||||||
|
url: |
||||||
|
url of the apk file to download and install. |
||||||
|
|
Loading…
Reference in new issue