Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 12a22e43 authored by Michael W's avatar Michael W
Browse files

Updater: Switch to destination selector

* Let the user decide where to store the file
* That way it's not located in /storage/emulated/0/Android/data/...
  ...org.lineageos.updater/files/LineageOS updates/*.zip
  -> The user knows where the file is stored
  -> We don't have to care about WRITE_EXTERNAL_STORAGE etc
* Remove the cancel button - after closing the file stream we loose
  permission to access it, therefore can't delete it anymore
  -> Let the user handle deletion manually
* Since we don't use WRITE_EXTERNAL_STORAGE anymore, remove it from
  Manifest and also remove PermissionUtils (+calls) - we can now export
  immediately.
  -> This also solves the
  "TODO: start exporting once the permission has been granted"

Change-Id: I50afa403f2803569aa9def807ea20ee72c582284
parent 2623730b
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -10,7 +10,6 @@
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.RECOVERY" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="false"
+18 −45
Original line number Diff line number Diff line
@@ -17,9 +17,10 @@ package org.lineageos.updater;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
@@ -40,10 +41,9 @@ public class ExportUpdateService extends Service {
    private static final int NOTIFICATION_ID = 16;

    public static final String ACTION_START_EXPORTING = "start_exporting";
    public static final String ACTION_STOP_EXPORTING = "stop_exporting";

    public static final String EXTRA_SOURCE_FILE = "source_file";
    public static final String EXTRA_DEST_FILE = "dest_file";
    public static final String EXTRA_DEST_URI = "dest_uri";

    private static final String EXPORT_NOTIFICATION_CHANNEL =
            "export_notification_channel";
@@ -51,7 +51,6 @@ public class ExportUpdateService extends Service {
    private volatile boolean mIsExporting = false;

    private Thread mExportThread;
    private ExportRunnable mExportRunnable;

    @Override
    public IBinder onBind(Intent intent) {
@@ -68,20 +67,8 @@ public class ExportUpdateService extends Service {
            }
            mIsExporting = true;
            File source = (File) intent.getSerializableExtra(EXTRA_SOURCE_FILE);
            File destination = (File) intent.getSerializableExtra(EXTRA_DEST_FILE);
            Uri destination = intent.getParcelableExtra(EXTRA_DEST_URI);
            startExporting(source, destination);
        } else if (ACTION_STOP_EXPORTING.equals(intent.getAction())) {
            if (mIsExporting) {
                mExportThread.interrupt();
                stopForeground(true);
                try {
                    mExportThread.join();
                } catch (InterruptedException e) {
                    Log.e(TAG, "Error while waiting for thread");
                }
                mExportRunnable.cleanUp();
                mIsExporting = false;
            }
        } else {
            Log.e(TAG, "No action specified");
        }
@@ -94,15 +81,17 @@ public class ExportUpdateService extends Service {
    }

    private class ExportRunnable implements Runnable {
        private final ContentResolver mContentResolver;
        private final File mSource;
        private final File mDestination;
        private final Uri mDestination;
        private final FileUtils.ProgressCallBack mProgressCallBack;
        private final Runnable mRunnableComplete;
        private final Runnable mRunnableFailed;

        private ExportRunnable(File source, File destination,
        private ExportRunnable(ContentResolver cr, File source, Uri destination,
                               FileUtils.ProgressCallBack progressCallBack,
                               Runnable runnableComplete, Runnable runnableFailed) {
            mContentResolver = cr;
            mSource = source;
            mDestination = destination;
            mProgressCallBack = progressCallBack;
@@ -113,7 +102,7 @@ public class ExportUpdateService extends Service {
        @Override
        public void run() {
            try {
                FileUtils.copyFile(mSource, mDestination, mProgressCallBack);
                FileUtils.copyFile(mContentResolver, mSource, mDestination, mProgressCallBack);
                mIsExporting = false;
                if (!mExportThread.isInterrupted()) {
                    Log.d(TAG, "Completed");
@@ -129,14 +118,10 @@ public class ExportUpdateService extends Service {
                stopSelf();
            }
        }

        private void cleanUp() {
            //noinspection ResultOfMethodCallIgnored
            mDestination.delete();
        }
    }

    private void startExporting(File source, File destination) {
    private void startExporting(File source, Uri destination) {
        final String fileName = FileUtils.queryName(getContentResolver(), destination);
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        NotificationChannel notificationChannel = new NotificationChannel(
                EXPORT_NOTIFICATION_CHANNEL,
@@ -149,12 +134,9 @@ public class ExportUpdateService extends Service {
        NotificationCompat.BigTextStyle notificationStyle = new NotificationCompat.BigTextStyle();
        notificationBuilder.setContentTitle(getString(R.string.dialog_export_title));
        notificationStyle.setBigContentTitle(getString(R.string.dialog_export_title));
        notificationStyle.bigText(destination.getName());
        notificationStyle.bigText(fileName);
        notificationBuilder.setStyle(notificationStyle);
        notificationBuilder.setSmallIcon(R.drawable.ic_system_update);
        notificationBuilder.addAction(android.R.drawable.ic_media_pause,
                getString(android.R.string.cancel),
                getStopPendingIntent());

        FileUtils.ProgressCallBack progressCallBack = new FileUtils.ProgressCallBack() {
            private long mLastUpdate = -1;
@@ -183,8 +165,7 @@ public class ExportUpdateService extends Service {
            notificationBuilder.setContentTitle(
                    getString(R.string.notification_export_success));
            notificationBuilder.setProgress(0, 0, false);
            notificationBuilder.setContentText(destination.getName());
            notificationBuilder.mActions.clear();
            notificationBuilder.setContentText(fileName);
            notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
            stopForeground(STOP_FOREGROUND_DETACH);
        };
@@ -197,21 +178,13 @@ public class ExportUpdateService extends Service {
                    getString(R.string.notification_export_fail));
            notificationBuilder.setProgress(0, 0, false);
            notificationBuilder.setContentText(null);
            notificationBuilder.mActions.clear();
            notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
            stopForeground(STOP_FOREGROUND_DETACH);
        };

        mExportRunnable = new ExportRunnable(source, destination, progressCallBack,
                runnableComplete, runnableFailed);
        mExportThread = new Thread(mExportRunnable);
        ExportRunnable exportRunnable = new ExportRunnable(getContentResolver(), source,
                destination, progressCallBack, runnableComplete, runnableFailed);
        mExportThread = new Thread(exportRunnable);
        mExportThread.start();
    }

    private PendingIntent getStopPendingIntent() {
        final Intent intent = new Intent(this, ExportUpdateService.class);
        intent.setAction(ACTION_STOP_EXPORTING);
        return PendingIntent.getService(this, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
    }
}
+33 −13
Original line number Diff line number Diff line
@@ -15,10 +15,12 @@
 */
package org.lineageos.updater;

import android.app.Activity;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.PowerManager;
import android.preference.PreferenceManager;
@@ -38,6 +40,8 @@ import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ContextThemeWrapper;
@@ -52,13 +56,11 @@ import org.lineageos.updater.controller.UpdaterController;
import org.lineageos.updater.controller.UpdaterService;
import org.lineageos.updater.misc.BuildInfoUtils;
import org.lineageos.updater.misc.Constants;
import org.lineageos.updater.misc.PermissionsUtils;
import org.lineageos.updater.misc.StringGenerator;
import org.lineageos.updater.misc.Utils;
import org.lineageos.updater.model.UpdateInfo;
import org.lineageos.updater.model.UpdateStatus;

import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.NumberFormat;
@@ -81,6 +83,8 @@ public class UpdatesListAdapter extends RecyclerView.Adapter<UpdatesListAdapter.

    private AlertDialog infoDialog;

    private UpdateInfo mToBeExported = null;

    private enum Action {
        DOWNLOAD,
        PAUSE,
@@ -539,12 +543,7 @@ public class UpdatesListAdapter extends RecyclerView.Adapter<UpdatesListAdapter.
                        mActivity.getString(R.string.toast_download_url_copied));
                return true;
            } else if (itemId == R.id.menu_export_update) {
                // TODO: start exporting once the permission has been granted
                boolean hasPermission = PermissionsUtils.checkAndRequestStoragePermission(
                        mActivity, 0);
                if (hasPermission) {
                exportUpdate(update);
                }
                return true;
            }
            return false;
@@ -555,14 +554,35 @@ public class UpdatesListAdapter extends RecyclerView.Adapter<UpdatesListAdapter.
    }

    private void exportUpdate(UpdateInfo update) {
        File dest = new File(Utils.getExportPath(mActivity), update.getName());
        if (dest.exists()) {
            dest = Utils.appendSequentialNumber(dest);
        if (mActivity == null) {
            return;
        }
        mToBeExported = update;
        ActivityResultLauncher<Intent> resultLauncher = mActivity.registerForActivityResult(
                new ActivityResultContracts.StartActivityForResult(),
                result -> {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        Intent intent = result.getData();
                        if (intent != null) {
                            Uri uri = intent.getData();
                            exportUpdate(uri);
                        }
                    }
                });

        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("application/zip");
        intent.putExtra(Intent.EXTRA_TITLE, update.getName());

        resultLauncher.launch(intent);
    }

    private void exportUpdate(Uri uri) {
        Intent intent = new Intent(mActivity, ExportUpdateService.class);
        intent.setAction(ExportUpdateService.ACTION_START_EXPORTING);
        intent.putExtra(ExportUpdateService.EXTRA_SOURCE_FILE, update.getFile());
        intent.putExtra(ExportUpdateService.EXTRA_DEST_FILE, dest);
        intent.putExtra(ExportUpdateService.EXTRA_SOURCE_FILE, mToBeExported.getFile());
        intent.putExtra(ExportUpdateService.EXTRA_DEST_URI, uri);
        mActivity.startService(intent);
    }

+36 −0
Original line number Diff line number Diff line
@@ -15,8 +15,15 @@
 */
package org.lineageos.updater.misc;

import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;

import androidx.annotation.NonNull;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -92,4 +99,33 @@ public class FileUtils {
            throw e;
        }
    }

    public static void copyFile(ContentResolver cr, File sourceFile, Uri destUri,
                                ProgressCallBack progressCallBack) throws IOException {
        try (FileChannel sourceChannel = new FileInputStream(sourceFile).getChannel();
             ParcelFileDescriptor pfd = cr.openFileDescriptor(destUri, "w");
             FileChannel destChannel = new FileOutputStream(pfd.getFileDescriptor()).getChannel()) {
            if (progressCallBack != null) {
                ReadableByteChannel readableByteChannel = new CallbackByteChannel(sourceChannel,
                        sourceFile.length(), progressCallBack);
                destChannel.transferFrom(readableByteChannel, 0, sourceChannel.size());
            } else {
                destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
            }
        } catch (IOException e) {
            Log.e(TAG, "Could not copy file", e);
            throw e;
        }
    }

    public static String queryName(@NonNull ContentResolver resolver, Uri uri) {
        try (Cursor returnCursor = resolver.query(uri, null, null, null, null)) {
            returnCursor.moveToFirst();
            int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
            return returnCursor.getString(nameIndex);
        } catch (Exception e) {
            // ignore
            return null;
        }
    }
}
+0 −69
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The LineageOS Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.lineageos.updater.misc;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;

import java.util.ArrayList;
import java.util.List;

public class PermissionsUtils {

    public static boolean hasPermission(Context context, String permission) {
        int permissionState = context.checkSelfPermission(permission);
        return permissionState == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Check the given permissions and requests them if needed.
     *
     * @param activity The target activity
     * @param permissions The permissions to check
     * @param requestCode @see Activity#requestPermissions(String[] , int)
     * @return true if the permission is granted, false otherwise
     */
    public static boolean checkAndRequestPermissions(final Activity activity,
            final String[] permissions, final int requestCode) {
        List<String> permissionsList = new ArrayList<>();
        for (String permission : permissions) {
            if (!hasPermission(activity, permission)) {
                permissionsList.add(permission);
            }
        }
        if (permissionsList.size() == 0) {
            return true;
        } else {
            String[] permissionsArray = new String[permissionsList.size()];
            permissionsArray = permissionsList.toArray(permissionsArray);
            activity.requestPermissions(permissionsArray, requestCode);
            return false;
        }
    }

    /**
     * Check and request the write external storage permission
     *
     * @see #checkAndRequestPermissions(Activity, String[], int)
     */
    public static boolean checkAndRequestStoragePermission(Activity activity, int requestCode) {
        return checkAndRequestPermissions(activity,
                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                requestCode);
    }
}