Add self-updater for github builds
This commit is contained in:
@@ -33,6 +33,9 @@ android {
|
|||||||
initWith release
|
initWith release
|
||||||
versionNameSuffix "-beta"
|
versionNameSuffix "-beta"
|
||||||
}
|
}
|
||||||
|
githubRelease{
|
||||||
|
initWith release
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
@@ -46,6 +49,9 @@ android {
|
|||||||
appcenterPublicBeta{
|
appcenterPublicBeta{
|
||||||
setRoot "src/appcenter"
|
setRoot "src/appcenter"
|
||||||
}
|
}
|
||||||
|
githubRelease{
|
||||||
|
setRoot "src/github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lintOptions{
|
lintOptions{
|
||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
|
|||||||
21
mastodon/src/github/AndroidManifest.xml
Normal file
21
mastodon/src/github/AndroidManifest.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.joinmastodon.android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$InstallerStatusReceiver" android:exported="false"/>-->
|
||||||
|
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$AfterUpdateRestartReceiver" android:exported="true" android:enabled="false">-->
|
||||||
|
<!-- <intent-filter>-->
|
||||||
|
<!-- <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>-->
|
||||||
|
<!-- </intent-filter>-->
|
||||||
|
<!-- </receiver>-->
|
||||||
|
<provider
|
||||||
|
android:authorities="${applicationId}.self_update_provider"
|
||||||
|
android:name=".updater.SelfUpdateContentProvider"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:exported="false"/>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
package org.joinmastodon.android.updater;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.DownloadManager;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageInstaller;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.BuildConfig;
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.MastodonApp;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
|
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||||
|
private static final long CHECK_PERIOD=24*3600*1000L;
|
||||||
|
private static final String TAG="GithubSelfUpdater";
|
||||||
|
|
||||||
|
private UpdateState state=UpdateState.NO_UPDATE;
|
||||||
|
private UpdateInfo info;
|
||||||
|
private long downloadID;
|
||||||
|
private BroadcastReceiver downloadCompletionReceiver=new BroadcastReceiver(){
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent){
|
||||||
|
if(downloadID!=0 && intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)==downloadID){
|
||||||
|
MastodonApp.context.unregisterReceiver(this);
|
||||||
|
setState(UpdateState.DOWNLOADED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public GithubSelfUpdaterImpl(){
|
||||||
|
SharedPreferences prefs=getPrefs();
|
||||||
|
int checkedByBuild=prefs.getInt("checkedByBuild", 0);
|
||||||
|
if(prefs.contains("version") && checkedByBuild==BuildConfig.VERSION_CODE){
|
||||||
|
info=new UpdateInfo();
|
||||||
|
info.version=prefs.getString("version", null);
|
||||||
|
info.size=prefs.getLong("apkSize", 0);
|
||||||
|
downloadID=prefs.getLong("downloadID", 0);
|
||||||
|
if(downloadID==0 || !getUpdateApkFile().exists()){
|
||||||
|
state=UpdateState.UPDATE_AVAILABLE;
|
||||||
|
}else{
|
||||||
|
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||||
|
state=dm.getUriForDownloadedFile(downloadID)==null ? UpdateState.DOWNLOADING : UpdateState.DOWNLOADED;
|
||||||
|
if(state==UpdateState.DOWNLOADING){
|
||||||
|
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else if(checkedByBuild!=BuildConfig.VERSION_CODE && checkedByBuild>0){
|
||||||
|
// We are in a new version, running for the first time after update. Gotta clean things up.
|
||||||
|
long id=getPrefs().getLong("downloadID", 0);
|
||||||
|
if(id!=0){
|
||||||
|
MastodonApp.context.getSystemService(DownloadManager.class).remove(id);
|
||||||
|
}
|
||||||
|
getUpdateApkFile().delete();
|
||||||
|
getPrefs().edit()
|
||||||
|
.remove("apkSize")
|
||||||
|
.remove("version")
|
||||||
|
.remove("apkURL")
|
||||||
|
.remove("checkedByBuild")
|
||||||
|
.remove("downloadID")
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SharedPreferences getPrefs(){
|
||||||
|
return MastodonApp.context.getSharedPreferences("githubUpdater", Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeCheckForUpdates(){
|
||||||
|
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||||
|
return;
|
||||||
|
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
|
||||||
|
if(timeSinceLastCheck>CHECK_PERIOD){
|
||||||
|
setState(UpdateState.CHECKING);
|
||||||
|
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actuallyCheckForUpdates(){
|
||||||
|
Request req=new Request.Builder()
|
||||||
|
.url("https://api.github.com/repos/mastodon/mastodon-android/releases/latest")
|
||||||
|
.build();
|
||||||
|
Call call=MastodonAPIController.getHttpClient().newCall(req);
|
||||||
|
try(Response resp=call.execute()){
|
||||||
|
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
||||||
|
String tag=obj.get("tag_name").getAsString();
|
||||||
|
Matcher matcher=Pattern.compile("v(\\d+)\\.(\\d+)\\.(\\d+)").matcher(tag);
|
||||||
|
if(!matcher.find()){
|
||||||
|
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=Integer.parseInt(matcher.group(3));
|
||||||
|
String[] currentParts=BuildConfig.VERSION_NAME.split("\\.");
|
||||||
|
int curMajor=Integer.parseInt(currentParts[0]), curMinor=Integer.parseInt(currentParts[1]), curRevision=Integer.parseInt(currentParts[2]);
|
||||||
|
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||||
|
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||||
|
if(newVersion>curVersion || BuildConfig.DEBUG){
|
||||||
|
String version=newMajor+"."+newMinor+"."+newRevision;
|
||||||
|
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||||
|
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||||
|
JsonObject asset=el.getAsJsonObject();
|
||||||
|
if("application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
|
||||||
|
long size=asset.get("size").getAsLong();
|
||||||
|
String url=asset.get("browser_download_url").getAsString();
|
||||||
|
|
||||||
|
UpdateInfo info=new UpdateInfo();
|
||||||
|
info.size=size;
|
||||||
|
info.version=version;
|
||||||
|
this.info=info;
|
||||||
|
|
||||||
|
getPrefs().edit()
|
||||||
|
.putLong("apkSize", size)
|
||||||
|
.putString("version", version)
|
||||||
|
.putString("apkURL", url)
|
||||||
|
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
|
||||||
|
.remove("downloadID")
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
|
||||||
|
}catch(Exception x){
|
||||||
|
Log.w(TAG, "actuallyCheckForUpdates", x);
|
||||||
|
}finally{
|
||||||
|
setState(info==null ? UpdateState.NO_UPDATE : UpdateState.UPDATE_AVAILABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setState(UpdateState state){
|
||||||
|
this.state=state;
|
||||||
|
E.post(new SelfUpdateStateChangedEvent(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UpdateState getState(){
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UpdateInfo getUpdateInfo(){
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getUpdateApkFile(){
|
||||||
|
return new File(MastodonApp.context.getExternalCacheDir(), "update.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void downloadUpdate(){
|
||||||
|
if(state==UpdateState.DOWNLOADING)
|
||||||
|
throw new IllegalStateException();
|
||||||
|
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||||
|
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||||
|
downloadID=dm.enqueue(
|
||||||
|
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
|
||||||
|
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
|
||||||
|
);
|
||||||
|
getPrefs().edit().putLong("downloadID", downloadID).apply();
|
||||||
|
setState(UpdateState.DOWNLOADING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void installUpdate(Activity activity){
|
||||||
|
if(state!=UpdateState.DOWNLOADED)
|
||||||
|
throw new IllegalStateException();
|
||||||
|
Uri uri;
|
||||||
|
Intent intent=new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||||
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||||
|
uri=new Uri.Builder().scheme("content").authority(activity.getPackageName()+".self_update_provider").path("update.apk").build();
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
}else{
|
||||||
|
uri=Uri.fromFile(getUpdateApkFile());
|
||||||
|
}
|
||||||
|
intent.setDataAndType(uri, "application/vnd.android.package-archive");
|
||||||
|
activity.startActivity(intent);
|
||||||
|
|
||||||
|
// TODO figure out how to restart the app when updating via this new API
|
||||||
|
/*
|
||||||
|
PackageInstaller installer=activity.getPackageManager().getPackageInstaller();
|
||||||
|
try{
|
||||||
|
final int sid=installer.createSession(new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL));
|
||||||
|
installer.registerSessionCallback(new PackageInstaller.SessionCallback(){
|
||||||
|
@Override
|
||||||
|
public void onCreated(int i){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBadgingChanged(int i){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActiveChanged(int i, boolean b){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(int id, float progress){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFinished(int id, boolean success){
|
||||||
|
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
|
||||||
|
PackageInstaller.Session session=installer.openSession(sid);
|
||||||
|
try(OutputStream out=session.openWrite("mastodon.apk", 0, info.size); InputStream in=new FileInputStream(getUpdateApkFile())){
|
||||||
|
byte[] buffer=new byte[16384];
|
||||||
|
int read;
|
||||||
|
while((read=in.read(buffer))>0){
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// PendingIntent intent=PendingIntent.getBroadcast(activity, 1, new Intent(activity, InstallerStatusReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||||
|
PendingIntent intent=PendingIntent.getActivity(activity, 1, new Intent(activity, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
session.commit(intent.getIntentSender());
|
||||||
|
}catch(IOException x){
|
||||||
|
Log.w(TAG, "installUpdate", x);
|
||||||
|
Toast.makeText(activity, x.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getDownloadProgress(){
|
||||||
|
if(state!=UpdateState.DOWNLOADING)
|
||||||
|
throw new IllegalStateException();
|
||||||
|
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||||
|
try(Cursor cursor=dm.query(new DownloadManager.Query().setFilterById(downloadID))){
|
||||||
|
if(cursor.moveToFirst()){
|
||||||
|
long loaded=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
|
||||||
|
long total=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
|
||||||
|
// Log.d(TAG, "getDownloadProgress: "+loaded+" of "+total);
|
||||||
|
return total>0 ? (float)loaded/total : 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelDownload(){
|
||||||
|
if(state!=UpdateState.DOWNLOADING)
|
||||||
|
throw new IllegalStateException();
|
||||||
|
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||||
|
dm.remove(downloadID);
|
||||||
|
downloadID=0;
|
||||||
|
getPrefs().edit().remove("downloadID").apply();
|
||||||
|
setState(UpdateState.UPDATE_AVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleIntentFromInstaller(Intent intent, Activity activity){
|
||||||
|
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
|
||||||
|
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
|
||||||
|
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||||
|
activity.startActivity(confirmIntent);
|
||||||
|
}else if(status!=PackageInstaller.STATUS_SUCCESS){
|
||||||
|
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||||
|
Toast.makeText(activity, activity.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent){
|
||||||
|
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
|
||||||
|
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
|
||||||
|
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||||
|
context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||||
|
}else if(status!=PackageInstaller.STATUS_SUCCESS){
|
||||||
|
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||||
|
Toast.makeText(context, context.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AfterUpdateRestartReceiver extends BroadcastReceiver{
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent){
|
||||||
|
if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())){
|
||||||
|
context.getPackageManager().setComponentEnabledSetting(new ComponentName(context, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||||
|
Toast.makeText(context, R.string.update_installed, Toast.LENGTH_SHORT).show();
|
||||||
|
Intent restartIntent=new Intent(context, MainActivity.class)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
.setPackage(context.getPackageName());
|
||||||
|
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.P){
|
||||||
|
context.startActivity(restartIntent);
|
||||||
|
}else{
|
||||||
|
// Bypass activity starting restrictions by starting it from a notification
|
||||||
|
NotificationManager nm=context.getSystemService(NotificationManager.class);
|
||||||
|
NotificationChannel chan=new NotificationChannel("selfUpdateRestart", context.getString(R.string.update_installed), NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
nm.createNotificationChannel(chan);
|
||||||
|
Notification n=new Notification.Builder(context, "selfUpdateRestart")
|
||||||
|
.setContentTitle(context.getString(R.string.update_installed))
|
||||||
|
.setContentIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
|
||||||
|
.setFullScreenIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), true)
|
||||||
|
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||||
|
.build();
|
||||||
|
nm.notify(1, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package org.joinmastodon.android.updater;
|
||||||
|
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public class SelfUpdateContentProvider extends ContentProvider{
|
||||||
|
@Override
|
||||||
|
public boolean onCreate(){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getType(@NonNull Uri uri){
|
||||||
|
if(isCorrectUri(uri))
|
||||||
|
return "application/vnd.android.package-archive";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
|
||||||
|
if(isCorrectUri(uri)){
|
||||||
|
return ParcelFileDescriptor.open(((GithubSelfUpdaterImpl)GithubSelfUpdater.getInstance()).getUpdateApkFile(), ParcelFileDescriptor.MODE_READ_ONLY);
|
||||||
|
}
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCorrectUri(Uri uri){
|
||||||
|
return "/update.apk".equals(uri.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.Manifest;
|
|||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageInstaller;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -20,6 +21,7 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
|||||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
@@ -73,6 +75,8 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
try{
|
try{
|
||||||
Class.forName("org.joinmastodon.android.AppCenterWrapper").getMethod("init", Application.class).invoke(null, getApplication());
|
Class.forName("org.joinmastodon.android.AppCenterWrapper").getMethod("init", Application.class).invoke(null, getApplication());
|
||||||
}catch(ClassNotFoundException|NoSuchMethodException|IllegalAccessException|InvocationTargetException ignore){}
|
}catch(ClassNotFoundException|NoSuchMethodException|IllegalAccessException|InvocationTargetException ignore){}
|
||||||
|
}else if(GithubSelfUpdater.needSelfUpdating()){
|
||||||
|
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +105,9 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
}
|
}
|
||||||
}else if(intent.getBooleanExtra("compose", false)){
|
}else if(intent.getBooleanExtra("compose", false)){
|
||||||
showCompose();
|
showCompose();
|
||||||
}
|
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||||
|
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFragmentForNotification(Notification notification, String accountID){
|
private void showFragmentForNotification(Notification notification, String accountID){
|
||||||
|
|||||||
@@ -197,4 +197,8 @@ public class MastodonAPIController{
|
|||||||
public static void runInBackground(Runnable action){
|
public static void runInBackground(Runnable action){
|
||||||
thread.postRunnable(action, 0);
|
thread.postRunnable(action, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static OkHttpClient getHttpClient(){
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
|
|
||||||
|
public class SelfUpdateStateChangedEvent{
|
||||||
|
public final GithubSelfUpdater.UpdateState state;
|
||||||
|
|
||||||
|
public SelfUpdateStateChangedEvent(GithubSelfUpdater.UpdateState state){
|
||||||
|
this.state=state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,9 @@ package org.joinmastodon.android.fragments;
|
|||||||
|
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.Outline;
|
import android.graphics.Outline;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -18,20 +14,14 @@ import android.view.WindowInsets;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.MainActivity;
|
|
||||||
import org.joinmastodon.android.MastodonApp;
|
|
||||||
import org.joinmastodon.android.PushNotificationReceiver;
|
import org.joinmastodon.android.PushNotificationReceiver;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||||
import org.joinmastodon.android.fragments.discover.SearchFragment;
|
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.TabBar;
|
import org.joinmastodon.android.ui.views.TabBar;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
@@ -41,15 +31,12 @@ import java.util.ArrayList;
|
|||||||
import androidx.annotation.IdRes;
|
import androidx.annotation.IdRes;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
import me.grishka.appkit.Nav;
|
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
|
||||||
import me.grishka.appkit.fragments.AppKitFragment;
|
import me.grishka.appkit.fragments.AppKitFragment;
|
||||||
import me.grishka.appkit.fragments.LoaderFragment;
|
import me.grishka.appkit.fragments.LoaderFragment;
|
||||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.BottomSheet;
|
|
||||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||||
|
|
||||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||||
@@ -141,7 +128,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}else{
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ import android.widget.Toolbar;
|
|||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
@@ -33,6 +35,7 @@ import org.joinmastodon.android.model.Status;
|
|||||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -101,6 +104,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(GithubSelfUpdater.needSelfUpdating()){
|
||||||
|
E.register(this);
|
||||||
|
updateUpdateState(GithubSelfUpdater.getInstance().getState());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -397,4 +405,22 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||||||
scrollToTop();
|
scrollToTop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView(){
|
||||||
|
super.onDestroyView();
|
||||||
|
if(GithubSelfUpdater.needSelfUpdating()){
|
||||||
|
E.unregister(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
|
||||||
|
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
|
||||||
|
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||||
|
updateUpdateState(ev.state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -14,15 +15,21 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.view.animation.LinearInterpolator;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.PopupMenu;
|
import android.widget.PopupMenu;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
import android.widget.RadioButton;
|
import android.widget.RadioButton;
|
||||||
import android.widget.Switch;
|
import android.widget.Switch;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
import org.joinmastodon.android.BuildConfig;
|
import org.joinmastodon.android.BuildConfig;
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.MainActivity;
|
import org.joinmastodon.android.MainActivity;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
@@ -31,11 +38,13 @@ import org.joinmastodon.android.api.MastodonAPIController;
|
|||||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||||
import org.joinmastodon.android.model.PushNotification;
|
import org.joinmastodon.android.model.PushNotification;
|
||||||
import org.joinmastodon.android.model.PushSubscription;
|
import org.joinmastodon.android.model.PushSubscription;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -47,7 +56,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
|
||||||
import me.grishka.appkit.imageloader.ImageCache;
|
import me.grishka.appkit.imageloader.ImageCache;
|
||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
@@ -73,6 +81,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
|
|
||||||
|
if(GithubSelfUpdater.needSelfUpdating()){
|
||||||
|
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||||
|
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||||
|
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
|
||||||
|
items.add(new UpdateItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.add(new HeaderItem(R.string.settings_theme));
|
items.add(new HeaderItem(R.string.settings_theme));
|
||||||
items.add(themeItem=new ThemeItem());
|
items.add(themeItem=new ThemeItem());
|
||||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||||
@@ -131,7 +147,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||||
// Add 32dp gaps between sections
|
// Add 32dp gaps between sections
|
||||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||||
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>0)
|
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>1)
|
||||||
outRect.top=V.dp(32);
|
outRect.top=V.dp(32);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -155,6 +171,20 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
if(GithubSelfUpdater.needSelfUpdating())
|
||||||
|
E.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView(){
|
||||||
|
super.onDestroyView();
|
||||||
|
if(GithubSelfUpdater.needSelfUpdating())
|
||||||
|
E.unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
|
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
|
||||||
GlobalUserPreferences.theme=theme;
|
GlobalUserPreferences.theme=theme;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
@@ -294,6 +324,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||||
|
if(items.get(0) instanceof UpdateItem item){
|
||||||
|
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(0);
|
||||||
|
if(holder instanceof UpdateViewHolder uvh){
|
||||||
|
uvh.bind(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static abstract class Item{
|
private static abstract class Item{
|
||||||
public abstract int getViewType();
|
public abstract int getViewType();
|
||||||
}
|
}
|
||||||
@@ -395,6 +435,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class UpdateItem extends Item{
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getViewType(){
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
|
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
@@ -408,6 +456,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
case 4 -> new TextViewHolder();
|
case 4 -> new TextViewHolder();
|
||||||
case 5 -> new HeaderViewHolder(true);
|
case 5 -> new HeaderViewHolder(true);
|
||||||
case 6 -> new FooterViewHolder();
|
case 6 -> new FooterViewHolder();
|
||||||
|
case 7 -> new UpdateViewHolder();
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -609,4 +658,74 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
text.setText(item.text);
|
text.setText(item.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
|
||||||
|
|
||||||
|
private final TextView text;
|
||||||
|
private final Button button;
|
||||||
|
private final ImageButton cancelBtn;
|
||||||
|
private final ProgressBar progress;
|
||||||
|
|
||||||
|
private ObjectAnimator rotationAnimator;
|
||||||
|
private Runnable progressUpdater=this::updateProgress;
|
||||||
|
|
||||||
|
public UpdateViewHolder(){
|
||||||
|
super(getActivity(), R.layout.item_settings_update, list);
|
||||||
|
text=findViewById(R.id.text);
|
||||||
|
button=findViewById(R.id.button);
|
||||||
|
cancelBtn=findViewById(R.id.cancel_btn);
|
||||||
|
progress=findViewById(R.id.progress);
|
||||||
|
button.setOnClickListener(v->{
|
||||||
|
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||||
|
switch(updater.getState()){
|
||||||
|
case UPDATE_AVAILABLE -> updater.downloadUpdate();
|
||||||
|
case DOWNLOADED -> updater.installUpdate(getActivity());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
|
||||||
|
rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
|
||||||
|
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||||
|
rotationAnimator.setDuration(1500);
|
||||||
|
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBind(UpdateItem item){
|
||||||
|
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||||
|
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||||
|
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||||
|
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||||
|
text.setText(getString(R.string.update_available, info.version));
|
||||||
|
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||||
|
}else{
|
||||||
|
text.setText(getString(R.string.update_ready, info.version));
|
||||||
|
button.setText(R.string.install_update);
|
||||||
|
}
|
||||||
|
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||||
|
rotationAnimator.start();
|
||||||
|
button.setVisibility(View.INVISIBLE);
|
||||||
|
cancelBtn.setVisibility(View.VISIBLE);
|
||||||
|
progress.setVisibility(View.VISIBLE);
|
||||||
|
updateProgress();
|
||||||
|
}else{
|
||||||
|
rotationAnimator.cancel();
|
||||||
|
button.setVisibility(View.VISIBLE);
|
||||||
|
cancelBtn.setVisibility(View.GONE);
|
||||||
|
progress.setVisibility(View.GONE);
|
||||||
|
progress.removeCallbacks(progressUpdater);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateProgress(){
|
||||||
|
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||||
|
if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
|
||||||
|
return;
|
||||||
|
int value=Math.round(progress.getMax()*updater.getDownloadProgress());
|
||||||
|
if(Build.VERSION.SDK_INT>=24)
|
||||||
|
progress.setProgress(value, true);
|
||||||
|
else
|
||||||
|
progress.setProgress(value);
|
||||||
|
progress.postDelayed(progressUpdater, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
|||||||
else
|
else
|
||||||
selectedIDs.add(id);
|
selectedIDs.add(id);
|
||||||
list.invalidate();
|
list.invalidate();
|
||||||
|
btn.setEnabled(!selectedIDs.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.joinmastodon.android.updater;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.BuildConfig;
|
||||||
|
|
||||||
|
public abstract class GithubSelfUpdater{
|
||||||
|
private static GithubSelfUpdater instance;
|
||||||
|
|
||||||
|
public static GithubSelfUpdater getInstance(){
|
||||||
|
if(instance==null){
|
||||||
|
try{
|
||||||
|
Class<?> c=Class.forName("org.joinmastodon.android.updater.GithubSelfUpdaterImpl");
|
||||||
|
instance=(GithubSelfUpdater) c.newInstance();
|
||||||
|
}catch(IllegalAccessException|InstantiationException|ClassNotFoundException ignored){
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean needSelfUpdating(){
|
||||||
|
return BuildConfig.BUILD_TYPE.equals("githubRelease");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void maybeCheckForUpdates();
|
||||||
|
|
||||||
|
public abstract GithubSelfUpdater.UpdateState getState();
|
||||||
|
|
||||||
|
public abstract GithubSelfUpdater.UpdateInfo getUpdateInfo();
|
||||||
|
|
||||||
|
public abstract void downloadUpdate();
|
||||||
|
|
||||||
|
public abstract void installUpdate(Activity activity);
|
||||||
|
|
||||||
|
public abstract float getDownloadProgress();
|
||||||
|
|
||||||
|
public abstract void cancelDownload();
|
||||||
|
|
||||||
|
public abstract void handleIntentFromInstaller(Intent intent, Activity activity);
|
||||||
|
|
||||||
|
public enum UpdateState{
|
||||||
|
NO_UPDATE,
|
||||||
|
CHECKING,
|
||||||
|
UPDATE_AVAILABLE,
|
||||||
|
DOWNLOADING,
|
||||||
|
DOWNLOADED
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UpdateInfo{
|
||||||
|
public String version;
|
||||||
|
public long size;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
mastodon/src/main/res/drawable/bg_settings_update.xml
Normal file
5
mastodon/src/main/res/drawable/bg_settings_update.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="?colorPollVoted"/>
|
||||||
|
<corners android:radius="4dp"/>
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorForeground">
|
||||||
|
<item android:id="@android:id/mask">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="#18000000"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="?android:colorBackground"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="16dp" android:height="16dp" android:viewportWidth="16" android:viewportHeight="16">
|
||||||
|
<path android:pathData="M2.397 2.554L2.47 2.47c0.266-0.267 0.683-0.29 0.976-0.073L3.53 2.47 8 6.939l4.47-4.47c0.293-0.292 0.767-0.292 1.06 0 0.293 0.294 0.293 0.768 0 1.061L9.061 8l4.47 4.47c0.266 0.266 0.29 0.683 0.072 0.976L13.53 13.53c-0.266 0.267-0.683 0.29-0.976 0.073L12.47 13.53 8 9.061l-4.47 4.47c-0.293 0.292-0.767 0.292-1.06 0-0.293-0.294-0.293-0.768 0-1.061L6.939 8l-4.47-4.47C2.204 3.264 2.18 2.847 2.398 2.554L2.47 2.47 2.397 2.554z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
||||||
10
mastodon/src/main/res/drawable/ic_settings_24_badged.xml
Normal file
10
mastodon/src/main/res/drawable/ic_settings_24_badged.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_fluent_settings_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
||||||
|
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
|
||||||
|
<solid android:color="@color/primary_600"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
12
mastodon/src/main/res/drawable/update_progress.xml
Normal file
12
mastodon/src/main/res/drawable/update_progress.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<shape
|
||||||
|
android:innerRadius="16dp"
|
||||||
|
android:shape="ring"
|
||||||
|
android:thickness="4dp"
|
||||||
|
android:useLevel="true">
|
||||||
|
<solid android:color="?colorSearchHint"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
75
mastodon/src/main/res/layout/item_settings_update.xml
Normal file
75
mastodon/src/main/res/layout/item_settings_update.xml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="64dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:background="@drawable/bg_settings_update"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:textAppearance="@style/m3_body_medium"
|
||||||
|
tools:text="@string/update_available"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|end"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:textColor="?colorAccentLight"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:stateListAnimator="@null"
|
||||||
|
tools:text="@string/install_update"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/cancel_btn"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="@drawable/bg_update_download_progress"
|
||||||
|
android:tint="?colorSearchHint"
|
||||||
|
android:contentDescription="@string/cancel"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:src="@drawable/ic_fluent_dismiss_16_filled"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:progressDrawable="@drawable/update_progress"
|
||||||
|
android:max="1000"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:indeterminateOnly="false"
|
||||||
|
android:indeterminate="false"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -377,4 +377,11 @@
|
|||||||
<string name="file_upload_time_remaining">%s remaining</string>
|
<string name="file_upload_time_remaining">%s remaining</string>
|
||||||
<string name="upload_error_connection_lost">Your device lost connection to the internet</string>
|
<string name="upload_error_connection_lost">Your device lost connection to the internet</string>
|
||||||
<string name="upload_processing">Processing…</string>
|
<string name="upload_processing">Processing…</string>
|
||||||
|
<!-- %s is version like 1.2.3 -->
|
||||||
|
<string name="update_available">Mastodon for Android %s is ready to download.</string>
|
||||||
|
<!-- %s is version like 1.2.3 -->
|
||||||
|
<string name="update_ready">Mastodon for Android %s is downloaded and ready to install.</string>
|
||||||
|
<!-- %s is file size -->
|
||||||
|
<string name="download_update">Download (%s)</string>
|
||||||
|
<string name="install_update">Install</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user