英文:
Updating my app remotely without Google Play access
问题
以下是翻译好的内容:
我正在为我们公司的内部使用开发一个应用程序,该应用程序将从网络收集性能统计数据,并将其发布到我们的 Grafana 服务器上。
在这种情况下,该应用程序可以正常运行,但存在一个问题:
应用程序将在数据中心的手机上运行,如果我们需要更新应用程序以添加功能,将很难访问它。
此外,该手机将无法访问互联网。因此,我将无法手动更新应用程序,也无法使用 Google Play 进行更新。
我考虑编写一个函数来检查静态 URL,当我们将更新的 apk 放在那里时,它将下载并安装它。
我编写了以下这个类(从另一个 Stackoverflow 问题中复制而来):
class updateApp extends AsyncTask<String, Void, String> {
protected String doInBackground(String... sUrl) {
File file = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),"updates");
if(!file.mkdir()){
}
File f = new File(file.getAbsolutePath(),"YourApp.apk");
Uri fUri = FileProvider.getUriForFile(MainActivity.this,"com.aktuna.vtv.monitor.fileprovider",f);
String path = fUri.getPath();
try {
URL url = new URL(sUrl[0]);
URLConnection connection = url.openConnection();
connection.connect();
InputStream input = new BufferedInputStream(url.openStream());
OutputStream output = new FileOutputStream(path);
byte data[] = new byte[1024];
int count;
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
}
output.flush();
output.close();
input.close();
} catch (Exception e) {
Log.e("YourApp", "好像出了点问题...");
Log.e("YourApp", e.getMessage());
}
return path;
}
@Override
protected void onPostExecute(String path) {
Intent i = new Intent();
i.setAction(Intent.ACTION_VIEW);
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
File file = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),"updates");
File f = new File(file.getAbsolutePath(),"YourApp.apk");
Uri fUri = FileProvider.getUriForFile(MainActivity.this,"com.aktuna.vtv.monitor.fileprovider",f);
i.setDataAndType(fUri, "application/vnd.android.package-archive" );
myCtx.startActivity(i);
}
}
它似乎成功下载了文件。然后将文件通过意图发送到安装程序(我可以看到这一点,因为会出现包安装程序的选择提示)。
但然后它没有安装新的 apk。
由于之前的 Stackoverflow 问题已经有 7 年了,我认为在新的 API 级别中可能会禁止没有用户交互的更新。
但我不确定如何进一步排除故障。
我也愿意听取任何建议,以实现这一目标,也许可以利用较旧的 API 级别,或者任何可以解决“通过内部静态 URL 进行无互联网访问的更新”问题的方法。
谢谢。
英文:
I am working on an app for my company's internal use which will collect performance stats from network and post them on our Grafana server.
The app works fine with this context, but there is a problem:
App will run on a phone at a datacenter and it will be very difficult to access it if we need to update the app for adding features.
Also the phone will not have internet access. So I won't be able to update the app manually , or using Google Play.
I thought of writing a function to check a static URL and when we put an updated apk there, it would download it and install.
I wrote this class (copying from another Stackoverflow question):
class updateApp extends AsyncTask<String, Void, String> {
protected String doInBackground(String... sUrl) {
File file = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),"updates");
if(!file.mkdir()){
}
File f = new File(file.getAbsolutePath(),"YourApp.apk");
Uri fUri = FileProvider.getUriForFile(MainActivity.this,"com.aktuna.vtv.monitor.fileprovider",f);
String path = fUri.getPath();
try {
URL url = new URL(sUrl[0]);
URLConnection connection = url.openConnection();
connection.connect();
InputStream input = new BufferedInputStream(url.openStream());
OutputStream output = new FileOutputStream(path);
byte data[] = new byte[1024];
int count;
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
}
output.flush();
output.close();
input.close();
} catch (Exception e) {
Log.e("YourApp", "Well that didn't work out so well...");
Log.e("YourApp", e.getMessage());
}
return path;
}
@Override
protected void onPostExecute(String path) {
Intent i = new Intent();
i.setAction(Intent.ACTION_VIEW);
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
File file = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),"updates");
File f = new File(file.getAbsolutePath(),"YourApp.apk");
Uri fUri = FileProvider.getUriForFile(MainActivity.this,"com.aktuna.vtv.monitor.fileprovider",f);
i.setDataAndType(fUri, "application/vnd.android.package-archive" );
myCtx.startActivity(i);
}
}
It seems to download the file successfully. And then it sends the file in the intent to the installer (I can see this because the packageinstaller selection prompt comes)
But then it does not install the new apk.
Since the previous Stackoverflow question is 7 years old, I thought that updating with no user interaction may be forbidden in new API levels.
But I am not sure.
How can I troubleshoot this further ?
Also, I am open to any suggestions to achieve this, maybe something making use of older API levels or anything that would solve the "updating with no internet access through an internal static URL" issue.
Thanks.
答案1
得分: 1
我按照@keag的建议进行了操作,而且取得了成功。
在设备上没有"root"的情况下,我将应用程序设置为"设备所有者"。
为此,我添加了一个设备管理员接收器类。SampleAdminReceiver.class:
import android.app.admin.DeviceAdminReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class SampleAdminReceiver extends DeviceAdminReceiver {
void showToast(Context context, CharSequence msg) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onEnabled(Context context, Intent intent) {
showToast(context, "Device admin enabled");
}
@Override
public void onDisabled(Context context, Intent intent) {
showToast(context, "Device admin disabled");
}
}
将接收器添加到清单文件中:
<receiver
android:name=".SampleAdminReceiver"
android:description="@string/app_name"
android:label="@string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN" >
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin_receiver" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
然后,使用adb界面运行以下dpm命令:
$ dpm set-device-owner com.sample.app/.SampleAdminReceiver
在清单文件中添加了以下权限:
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
使用以下函数,我能够从URL安装APK:
public static boolean installPackageX(final Context context, final String url)
throws IOException {
// 使用异步任务来运行安装包方法
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
try {
PackageInstaller packageInstaller = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
packageInstaller = context.getPackageManager().getPackageInstaller();
}
PackageInstaller.SessionParams params = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
}
// 设置参数
int sessionId = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sessionId = packageInstaller.createSession(params);
}
PackageInstaller.Session session = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session = packageInstaller.openSession(sessionId);
}
OutputStream out = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
out = session.openWrite("COSU", 0, -1);
}
// 从URL获取输入流
HttpURLConnection apkConn = (HttpURLConnection) new URL(url).openConnection();
InputStream in = apkConn.getInputStream();
byte[] buffer = new byte[65536];
int c;
while ((c = in.read(buffer)) != -1) {
out.write(buffer, 0, c);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session.fsync(out);
}
in.close();
out.close();
// 可以将此意图替换为应用程序安装完成时要运行的任何意图
// 假设你有一个名为InstallComplete的活动
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra("info", "somedata"); // 如果需要额外的数据..
Random generator = new Random();
PendingIntent i = PendingIntent.getActivity(context, generator.nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session.commit(i.getIntentSender());
}
} catch (Exception ex) {
ex.printStackTrace();
Log.e("AppStore", "安装应用程序时出错。错误信息为:" + ex.getMessage());
}
return null;
}
};
task.execute(null, null);
return true;
}
之后,只需自动化这个过程。
另外,对于从应用程序中删除"设备所有者"属性,以下代码是有用的:
DevicePolicyManager dpm = (DevicePolicyManager) getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
dpm.clearDeviceOwnerApp(getApplicationContext().getPackageName());
英文:
I followed recommendation from @keag and it worked.
1.
With no "root" on the device, I made the app "device-owner"
For this I added a device admin receiver class. SampleAdminReceiver.class:
import android.app.admin.DeviceAdminReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class SampleAdminReceiver extends DeviceAdminReceiver {
void showToast(Context context, CharSequence msg) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onEnabled(Context context, Intent intent) {
showToast(context, "Device admin enabled");
}
@Override
public void onDisabled(Context context, Intent intent) {
showToast(context, "Device admin disabled");
}
}
added receiver to the manifest:
<receiver
android:name=".SampleAdminReceiver"
android:description="@string/app_name"
android:label="@string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN" >
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin_receiver" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
Then using the adb interface I run the following dpm command:
$ dpm set-device-owner com.sample.app/.SampleAdminReceiver
2.
Added following permission to manifest :
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
3.
The with the following function I am able to install the apk from URL:
public static boolean installPackageX(final Context context, final String url)
throws IOException {
//Use an async task to run the install package method
AsyncTask<Void,Void,Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
try {
PackageInstaller packageInstaller = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
packageInstaller = context.getPackageManager().getPackageInstaller();
}
PackageInstaller.SessionParams params = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
}
// set params
int sessionId = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sessionId = packageInstaller.createSession(params);
}
PackageInstaller.Session session = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session = packageInstaller.openSession(sessionId);
}
OutputStream out = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
out = session.openWrite("COSU", 0, -1);
}
//get the input stream from the url
HttpURLConnection apkConn = (HttpURLConnection) new URL(url).openConnection();
InputStream in = apkConn.getInputStream();
byte[] buffer = new byte[65536];
int c;
while ((c = in.read(buffer)) != -1) {
out.write(buffer, 0, c);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session.fsync(out);
}
in.close();
out.close();
//you can replace this intent with whatever intent you want to be run when the applicaiton is finished installing
//I assume you have an activity called InstallComplete
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra("info", "somedata"); // for extra data if needed..
Random generator = new Random();
PendingIntent i = PendingIntent.getActivity(context, generator.nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session.commit(i.getIntentSender());
}
} catch (Exception ex){
ex.printStackTrace();
Log.e("AppStore","Error when installing application. Error is " + ex.getMessage());
}
return null;
}
};
task.execute(null,null);
return true;
}
After that, it is just a matter of automating the process.
Btw, following code in the app is useful for removing "device owner" property.
DevicePolicyManager dpm = (DevicePolicyManager) getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
dpm.clearDeviceOwnerApp(getApplicationContext().getPackageName());
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论