Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let user update the app #355

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ dependencies {
implementation ('com.github.bumptech.glide:glide:4.13.2') {
exclude group: "com.android.support"
}
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.2'
implementation 'com.squareup.okhttp3:okhttp:4.9.1'

fullImplementation 'com.google.firebase:firebase-analytics:19.0.1'
Expand Down Expand Up @@ -137,7 +138,6 @@ dependencies {
//noinspection AnnotationProcessorOnCompilePath
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

implementation 'org.ocpsoft.prettytime:prettytime:5.0.2.Final'

Expand Down
2 changes: 2 additions & 0 deletions app/src/full/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

<application
android:name=".OdyseeApp"
android:allowBackup="true"
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
package com.odysee.app.ui.findcontent;

import android.Manifest;
import android.annotation.SuppressLint;
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.InstallSourceInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
Expand All @@ -14,22 +26,36 @@

import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;

import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;
import com.odysee.app.BuildConfig;
import com.odysee.app.OdyseeApp;
import com.odysee.app.callable.ChannelLiveStatus;
import com.odysee.app.callable.Search;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.jetbrains.annotations.NotNull;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
Expand Down Expand Up @@ -110,6 +136,8 @@ public class AllContentFragment extends BaseFragment implements DownloadActionLi
private int wildWestIndex = -1;
private int moviesIndex = -1;

private long latestVersionCheck = 0L;

private void buildAndDisplayContentCategories() {
if (contentCategoriesDisplayed) {
return;
Expand Down Expand Up @@ -454,6 +482,9 @@ public void onResume() {
}

applyFilterForMutedChannels(Lbryio.mutedChannels);

// Check for app auto-update
checkAppUpdater();
}

public void onPause() {
Expand All @@ -466,6 +497,176 @@ public void onPause() {
super.onPause();
}

private void checkAppUpdater() {
if (BuildConfig.DEBUG) {
return;
}

Date now = new Date();
Context activity = getActivity();
// Check for a new version once in a day
if (activity != null && (now.getTime() - latestVersionCheck) > (24 * 3600 * 1000)) {
latestVersionCheck = now.getTime();

PackageManager pm = activity.getPackageManager();
try {
String installer;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
InstallSourceInfo installSourceInfo = pm.getInstallSourceInfo(activity.getPackageName());
installer = installSourceInfo.getInstallingPackageName();
} else {
//noinspection deprecation
kekkyojin marked this conversation as resolved.
Show resolved Hide resolved
installer = pm.getInstallerPackageName(activity.getPackageName());
}

List<String> packagesNotAutoUpdating = Arrays.asList("com.google.android.feedback", "com.android.ending", "org.fdroid.fdroid");
kekkyojin marked this conversation as resolved.
Show resolved Hide resolved

if (installer == null || !packagesNotAutoUpdating.contains(installer)) {
((OdyseeApp)getActivity().getApplication()).getExecutor().submit(new Runnable() {
@Override
public void run() {
checkRemoteLatestVersion(getView());
}
});
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
}

/**
* Checks for a new version on the remote server and then updates the app if a new one is available
* @param root the root view to show the snackbar
*/
private void checkRemoteLatestVersion(View root) {
/* JSON file format:
* {
* latest_version_code: integer,
* url: string including https and package file
* }
*/
Request.Builder builder = new Request.Builder();
builder = builder.url("http://apk.odysee.com/odysee-android/odysee-android-latest.json");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the APK server is at apk.odysee.tv.

Pinging @tzarebczan about this so the JSON file path can be discussed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a different domain because maybe they would prefer to use it in order to keep it separate from the non-native release. Anyway, I was also expecting to put there any server pointed at by the team.

Request request = builder.build();
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
Call call = okHttpClient.newCall(request);

call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.e("RemoteLatestVersion", e.getMessage());
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
ResponseBody body = response.body();

if (body != null) {
String respBody = body.string();

try {
JSONObject responseJson = new JSONObject(respBody);

long latestCode = responseJson.getLong("latest_version_code");
String latestUrl = responseJson.getString("url");

if (latestCode > BuildConfig.VERSION_CODE && !Helper.isNullOrEmpty(latestUrl)) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Snackbar snackbar = Snackbar.make(root.findViewById(R.id.coordinator_root), getResources().getString(R.string.snackbar_new_version_available), BaseTransientBottomBar.LENGTH_INDEFINITE);
snackbar.setAction(getResources().getString(R.string.snackbar_install_button), new View.OnClickListener() {
@Override
public void onClick(View view) {
Context context = getContext();
if (context != null) {
ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE);
String filePath = context.getExternalFilesDir(null).getAbsolutePath().concat("/odysee-android.apk");
final Uri uri = Uri.parse("file://" + filePath);
File f = new File(filePath);

if (f.exists()) {
f.delete();
}
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(latestUrl));
request.setDescription("Odysee new version available");
request.setTitle(getResources().getString(R.string.downloadmanager_downloading_new_version));

//set destination
request.setDestinationUri(uri);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);

// get download service and enqueue file
final DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
manager.enqueue(request);

//set BroadcastReceiver to install app when .apk is downloaded
BroadcastReceiver onComplete = new BroadcastReceiver() {
public void onReceive(Context ctxt, Intent intent) {
ContextCompat.checkSelfPermission(ctxt, Manifest.permission.READ_EXTERNAL_STORAGE);

installApk();
getContext().unregisterReceiver(this);
}
};

//register receiver for when .apk download is complete
context.registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
});
snackbar.show();
}
});
}
} catch (JSONException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

});
}

@SuppressLint("QueryPermissionsNeeded")
private void installApk() {
try {
Context context = getContext();
if (context != null) {
File file = new File(getContext().getExternalFilesDir(null).getAbsolutePath().concat("/odysee-android.apk"));
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
Uri downloaded_apk = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", file);
intent.setDataAndType(downloaded_apk, "application/vnd.android.package-archive");
List<ResolveInfo> resInfoList;

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) {
resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
} else {
//noinspection deprecation
kekkyojin marked this conversation as resolved.
Show resolved Hide resolved
resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
}
for (ResolveInfo resolveInfo : resInfoList) {
context.grantUriPermission(context.getApplicationContext().getPackageName() + ".provider", downloaded_apk, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
kekkyojin marked this conversation as resolved.
Show resolved Hide resolved
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
} else {
intent.setAction(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(intent);
}
} catch (Exception e) {
e.printStackTrace();
}
}

private Map<String, Object> buildContentOptions() {
Context context = getContext();
boolean canShowMatureContent = false;
Expand Down
Loading