| /* |
| * Copyright (C) 2017-2022 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; |
| |
| import android.annotation.SuppressLint; |
| import android.app.Activity; |
| import android.app.UiModeManager; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.res.Configuration; |
| import android.content.ServiceConnection; |
| import android.content.SharedPreferences; |
| import android.icu.text.DateFormat; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.SystemProperties; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuItem; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.animation.Animation; |
| import android.view.animation.LinearInterpolator; |
| import android.view.animation.RotateAnimation; |
| import android.widget.Spinner; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import androidx.activity.result.ActivityResultLauncher; |
| import androidx.activity.result.contract.ActivityResultContracts; |
| import androidx.appcompat.app.ActionBar; |
| import androidx.appcompat.app.AlertDialog; |
| import androidx.appcompat.widget.SwitchCompat; |
| import androidx.appcompat.widget.Toolbar; |
| import androidx.localbroadcastmanager.content.LocalBroadcastManager; |
| import androidx.preference.PreferenceManager; |
| import androidx.recyclerview.widget.LinearLayoutManager; |
| import androidx.recyclerview.widget.RecyclerView; |
| import androidx.recyclerview.widget.SimpleItemAnimator; |
| |
| import com.google.android.material.appbar.AppBarLayout; |
| import com.google.android.material.appbar.CollapsingToolbarLayout; |
| import com.google.android.material.snackbar.Snackbar; |
| |
| import org.json.JSONException; |
| import org.lineageos.updater.controller.UpdaterController; |
| import org.lineageos.updater.controller.UpdaterService; |
| import org.lineageos.updater.download.DownloadClient; |
| import org.lineageos.updater.misc.BuildInfoUtils; |
| import org.lineageos.updater.misc.Constants; |
| import org.lineageos.updater.misc.StringGenerator; |
| import org.lineageos.updater.misc.Utils; |
| import org.lineageos.updater.model.UpdateInfo; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.UUID; |
| |
| public class UpdatesActivity extends UpdatesListActivity { |
| |
| private static final String TAG = "UpdatesActivity"; |
| private UpdaterService mUpdaterService; |
| private BroadcastReceiver mBroadcastReceiver; |
| |
| private UpdatesListAdapter mAdapter; |
| |
| private View mRefreshIconView; |
| private RotateAnimation mRefreshAnimation; |
| |
| private boolean mIsTV; |
| |
| private UpdateInfo mToBeExported = null; |
| private final ActivityResultLauncher<Intent> mExportUpdate = registerForActivityResult( |
| new ActivityResultContracts.StartActivityForResult(), |
| result -> { |
| if (result.getResultCode() == Activity.RESULT_OK) { |
| Intent intent = result.getData(); |
| if (intent != null) { |
| Uri uri = intent.getData(); |
| exportUpdate(uri); |
| } |
| } |
| }); |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.activity_updates); |
| |
| UiModeManager uiModeManager = getSystemService(UiModeManager.class); |
| mIsTV = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; |
| |
| RecyclerView recyclerView = findViewById(R.id.recycler_view); |
| mAdapter = new UpdatesListAdapter(this); |
| recyclerView.setAdapter(mAdapter); |
| RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); |
| recyclerView.setLayoutManager(layoutManager); |
| RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); |
| if (animator instanceof SimpleItemAnimator) { |
| ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); |
| } |
| |
| mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (UpdaterController.ACTION_UPDATE_STATUS.equals(intent.getAction())) { |
| String downloadId = intent.getStringExtra(UpdaterController.EXTRA_DOWNLOAD_ID); |
| handleDownloadStatusChange(downloadId); |
| mAdapter.notifyItemChanged(downloadId); |
| } else if (UpdaterController.ACTION_DOWNLOAD_PROGRESS.equals(intent.getAction()) || |
| UpdaterController.ACTION_INSTALL_PROGRESS.equals(intent.getAction())) { |
| String downloadId = intent.getStringExtra(UpdaterController.EXTRA_DOWNLOAD_ID); |
| mAdapter.notifyItemChanged(downloadId); |
| } else if (UpdaterController.ACTION_UPDATE_REMOVED.equals(intent.getAction())) { |
| String downloadId = intent.getStringExtra(UpdaterController.EXTRA_DOWNLOAD_ID); |
| mAdapter.removeItem(downloadId); |
| } |
| } |
| }; |
| |
| if (!mIsTV) { |
| Toolbar toolbar = findViewById(R.id.toolbar); |
| setSupportActionBar(toolbar); |
| ActionBar actionBar = getSupportActionBar(); |
| if (actionBar != null) { |
| actionBar.setDisplayShowTitleEnabled(false); |
| actionBar.setDisplayHomeAsUpEnabled(true); |
| } |
| } |
| |
| TextView headerTitle = findViewById(R.id.header_title); |
| headerTitle.setText(getString(R.string.header_title_text, |
| BuildInfoUtils.getBuildVersion())); |
| |
| updateLastCheckedString(); |
| |
| TextView headerBuildVersion = findViewById(R.id.header_build_version); |
| headerBuildVersion.setText( |
| getString(R.string.header_android_version, Build.VERSION.RELEASE)); |
| |
| TextView headerBuildDate = findViewById(R.id.header_build_date); |
| headerBuildDate.setText(StringGenerator.getDateLocalizedUTC(this, |
| DateFormat.LONG, BuildInfoUtils.getBuildDateTimestamp())); |
| |
| if (!mIsTV) { |
| // Switch between header title and appbar title minimizing overlaps |
| final CollapsingToolbarLayout collapsingToolbar = findViewById(R.id.collapsing_toolbar); |
| final AppBarLayout appBar = findViewById(R.id.app_bar); |
| appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() { |
| boolean mIsShown = false; |
| |
| @Override |
| public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) { |
| int scrollRange = appBarLayout.getTotalScrollRange(); |
| if (!mIsShown && scrollRange + verticalOffset < 10) { |
| collapsingToolbar.setTitle(getString(R.string.display_name)); |
| mIsShown = true; |
| } else if (mIsShown && scrollRange + verticalOffset > 100) { |
| collapsingToolbar.setTitle(null); |
| mIsShown = false; |
| } |
| } |
| }); |
| |
| mRefreshAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, |
| Animation.RELATIVE_TO_SELF, 0.5f); |
| mRefreshAnimation.setInterpolator(new LinearInterpolator()); |
| mRefreshAnimation.setDuration(1000); |
| |
| if (!Utils.hasTouchscreen(this)) { |
| // This can't be collapsed without a touchscreen |
| appBar.setExpanded(false); |
| } |
| } else { |
| findViewById(R.id.refresh).setOnClickListener(v -> downloadUpdatesList(true)); |
| findViewById(R.id.preferences).setOnClickListener(v -> showPreferencesDialog()); |
| } |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| Intent intent = new Intent(this, UpdaterService.class); |
| startService(intent); |
| bindService(intent, mConnection, Context.BIND_AUTO_CREATE); |
| |
| IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(UpdaterController.ACTION_UPDATE_STATUS); |
| intentFilter.addAction(UpdaterController.ACTION_DOWNLOAD_PROGRESS); |
| intentFilter.addAction(UpdaterController.ACTION_INSTALL_PROGRESS); |
| intentFilter.addAction(UpdaterController.ACTION_UPDATE_REMOVED); |
| LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter); |
| } |
| |
| @Override |
| public void onStop() { |
| LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver); |
| if (mUpdaterService != null) { |
| unbindService(mConnection); |
| } |
| super.onStop(); |
| } |
| |
| @Override |
| public boolean onCreateOptionsMenu(Menu menu) { |
| getMenuInflater().inflate(R.menu.menu_toolbar, menu); |
| return super.onCreateOptionsMenu(menu); |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| int itemId = item.getItemId(); |
| if (itemId == R.id.menu_refresh) { |
| downloadUpdatesList(true); |
| return true; |
| } else if (itemId == R.id.menu_preferences) { |
| showPreferencesDialog(); |
| return true; |
| } else if (itemId == R.id.menu_show_changelog) { |
| Intent openUrl = new Intent(Intent.ACTION_VIEW, |
| Uri.parse(Utils.getChangelogURL(this))); |
| startActivity(openUrl); |
| return true; |
| } |
| return super.onOptionsItemSelected(item); |
| } |
| |
| @Override |
| public boolean onSupportNavigateUp() { |
| onBackPressed(); |
| return true; |
| } |
| |
| private final ServiceConnection mConnection = new ServiceConnection() { |
| |
| @Override |
| public void onServiceConnected(ComponentName className, |
| IBinder service) { |
| UpdaterService.LocalBinder binder = (UpdaterService.LocalBinder) service; |
| mUpdaterService = binder.getService(); |
| mAdapter.setUpdaterController(mUpdaterService.getUpdaterController()); |
| getUpdatesList(); |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName componentName) { |
| mAdapter.setUpdaterController(null); |
| mUpdaterService = null; |
| mAdapter.notifyDataSetChanged(); |
| } |
| }; |
| |
| private void loadUpdatesList(File jsonFile, boolean manualRefresh) |
| throws IOException, JSONException { |
| Log.d(TAG, "Adding remote updates"); |
| UpdaterController controller = mUpdaterService.getUpdaterController(); |
| boolean newUpdates = false; |
| |
| List<UpdateInfo> updates = Utils.parseJson(jsonFile, true); |
| List<String> updatesOnline = new ArrayList<>(); |
| for (UpdateInfo update : updates) { |
| newUpdates |= controller.addUpdate(update); |
| updatesOnline.add(update.getDownloadId()); |
| } |
| controller.setUpdatesAvailableOnline(updatesOnline, true); |
| |
| if (manualRefresh) { |
| showSnackbar( |
| newUpdates ? R.string.snack_updates_found : R.string.snack_no_updates_found, |
| Snackbar.LENGTH_SHORT); |
| } |
| |
| List<String> updateIds = new ArrayList<>(); |
| List<UpdateInfo> sortedUpdates = controller.getUpdates(); |
| if (sortedUpdates.isEmpty()) { |
| findViewById(R.id.no_new_updates_view).setVisibility(View.VISIBLE); |
| findViewById(R.id.recycler_view).setVisibility(View.GONE); |
| } else { |
| findViewById(R.id.no_new_updates_view).setVisibility(View.GONE); |
| findViewById(R.id.recycler_view).setVisibility(View.VISIBLE); |
| sortedUpdates.sort((u1, u2) -> Long.compare(u2.getTimestamp(), u1.getTimestamp())); |
| for (UpdateInfo update : sortedUpdates) { |
| updateIds.add(update.getDownloadId()); |
| } |
| mAdapter.setData(updateIds); |
| mAdapter.notifyDataSetChanged(); |
| } |
| } |
| |
| private void getUpdatesList() { |
| File jsonFile = Utils.getCachedUpdateList(this); |
| if (jsonFile.exists()) { |
| try { |
| loadUpdatesList(jsonFile, false); |
| Log.d(TAG, "Cached list parsed"); |
| } catch (IOException | JSONException e) { |
| Log.e(TAG, "Error while parsing json list", e); |
| } |
| } else { |
| downloadUpdatesList(false); |
| } |
| } |
| |
| private void processNewJson(File json, File jsonNew, boolean manualRefresh) { |
| try { |
| loadUpdatesList(jsonNew, manualRefresh); |
| SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); |
| long millis = System.currentTimeMillis(); |
| preferences.edit().putLong(Constants.PREF_LAST_UPDATE_CHECK, millis).apply(); |
| updateLastCheckedString(); |
| if (json.exists() && Utils.isUpdateCheckEnabled(this) && |
| Utils.checkForNewUpdates(json, jsonNew)) { |
| UpdatesCheckReceiver.updateRepeatingUpdatesCheck(this); |
| } |
| // In case we set a one-shot check because of a previous failure |
| UpdatesCheckReceiver.cancelUpdatesCheck(this); |
| //noinspection ResultOfMethodCallIgnored |
| jsonNew.renameTo(json); |
| } catch (IOException | JSONException e) { |
| Log.e(TAG, "Could not read json", e); |
| showSnackbar(R.string.snack_updates_check_failed, Snackbar.LENGTH_LONG); |
| } |
| } |
| |
| private void downloadUpdatesList(final boolean manualRefresh) { |
| final File jsonFile = Utils.getCachedUpdateList(this); |
| final File jsonFileTmp = new File(jsonFile.getAbsolutePath() + UUID.randomUUID()); |
| String url = Utils.getServerURL(this); |
| Log.d(TAG, "Checking " + url); |
| |
| DownloadClient.DownloadCallback callback = new DownloadClient.DownloadCallback() { |
| @Override |
| public void onFailure(final boolean cancelled) { |
| Log.e(TAG, "Could not download updates list"); |
| runOnUiThread(() -> { |
| if (!cancelled) { |
| showSnackbar(R.string.snack_updates_check_failed, Snackbar.LENGTH_LONG); |
| } |
| refreshAnimationStop(); |
| }); |
| } |
| |
| @Override |
| public void onResponse(DownloadClient.Headers headers) { |
| } |
| |
| @Override |
| public void onSuccess() { |
| runOnUiThread(() -> { |
| Log.d(TAG, "List downloaded"); |
| processNewJson(jsonFile, jsonFileTmp, manualRefresh); |
| refreshAnimationStop(); |
| }); |
| } |
| }; |
| |
| final DownloadClient downloadClient; |
| try { |
| downloadClient = new DownloadClient.Builder() |
| .setUrl(url) |
| .setDestination(jsonFileTmp) |
| .setDownloadCallback(callback) |
| .build(); |
| } catch (IOException exception) { |
| Log.e(TAG, "Could not build download client"); |
| showSnackbar(R.string.snack_updates_check_failed, Snackbar.LENGTH_LONG); |
| return; |
| } |
| |
| refreshAnimationStart(); |
| downloadClient.start(); |
| } |
| |
| private void updateLastCheckedString() { |
| final SharedPreferences preferences = |
| PreferenceManager.getDefaultSharedPreferences(this); |
| long lastCheck = preferences.getLong(Constants.PREF_LAST_UPDATE_CHECK, -1) / 1000; |
| String lastCheckString = getString(R.string.header_last_updates_check, |
| StringGenerator.getDateLocalized(this, DateFormat.LONG, lastCheck), |
| StringGenerator.getTimeLocalized(this, lastCheck)); |
| TextView headerLastCheck = findViewById(R.id.header_last_check); |
| headerLastCheck.setText(lastCheckString); |
| } |
| |
| private void handleDownloadStatusChange(String downloadId) { |
| UpdateInfo update = mUpdaterService.getUpdaterController().getUpdate(downloadId); |
| switch (update.getStatus()) { |
| case PAUSED_ERROR: |
| showSnackbar(R.string.snack_download_failed, Snackbar.LENGTH_LONG); |
| break; |
| case VERIFICATION_FAILED: |
| showSnackbar(R.string.snack_download_verification_failed, Snackbar.LENGTH_LONG); |
| break; |
| case VERIFIED: |
| showSnackbar(R.string.snack_download_verified, Snackbar.LENGTH_LONG); |
| break; |
| } |
| } |
| |
| @Override |
| public void exportUpdate(UpdateInfo update) { |
| mToBeExported = update; |
| |
| Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); |
| intent.addCategory(Intent.CATEGORY_OPENABLE); |
| intent.setType("application/zip"); |
| intent.putExtra(Intent.EXTRA_TITLE, update.getName()); |
| |
| mExportUpdate.launch(intent); |
| } |
| |
| private void exportUpdate(Uri uri) { |
| Intent intent = new Intent(this, ExportUpdateService.class); |
| intent.setAction(ExportUpdateService.ACTION_START_EXPORTING); |
| intent.putExtra(ExportUpdateService.EXTRA_SOURCE_FILE, mToBeExported.getFile()); |
| intent.putExtra(ExportUpdateService.EXTRA_DEST_URI, uri); |
| startService(intent); |
| } |
| |
| @Override |
| public void showSnackbar(int stringId, int duration) { |
| Snackbar.make(findViewById(R.id.main_container), stringId, duration).show(); |
| } |
| |
| private void refreshAnimationStart() { |
| if (!mIsTV) { |
| if (mRefreshIconView == null) { |
| mRefreshIconView = findViewById(R.id.menu_refresh); |
| } |
| if (mRefreshIconView != null) { |
| mRefreshAnimation.setRepeatCount(Animation.INFINITE); |
| mRefreshIconView.startAnimation(mRefreshAnimation); |
| mRefreshIconView.setEnabled(false); |
| } |
| } else { |
| findViewById(R.id.recycler_view).setVisibility(View.GONE); |
| findViewById(R.id.no_new_updates_view).setVisibility(View.GONE); |
| findViewById(R.id.refresh_progress).setVisibility(View.VISIBLE); |
| } |
| } |
| |
| private void refreshAnimationStop() { |
| if (!mIsTV) { |
| if (mRefreshIconView != null) { |
| mRefreshAnimation.setRepeatCount(0); |
| mRefreshIconView.setEnabled(true); |
| } |
| } else { |
| findViewById(R.id.refresh_progress).setVisibility(View.GONE); |
| if (mAdapter.getItemCount() > 0) { |
| findViewById(R.id.recycler_view).setVisibility(View.VISIBLE); |
| } else { |
| findViewById(R.id.no_new_updates_view).setVisibility(View.VISIBLE); |
| } |
| } |
| } |
| |
| @SuppressLint("ClickableViewAccessibility") |
| private void showPreferencesDialog() { |
| View view = LayoutInflater.from(this).inflate(R.layout.preferences_dialog, null); |
| Spinner autoCheckInterval = view.findViewById(R.id.preferences_auto_updates_check_interval); |
| SwitchCompat autoDelete = view.findViewById(R.id.preferences_auto_delete_updates); |
| SwitchCompat dataWarning = view.findViewById(R.id.preferences_mobile_data_warning); |
| SwitchCompat abPerfMode = view.findViewById(R.id.preferences_ab_perf_mode); |
| SwitchCompat updateRecovery = view.findViewById(R.id.preferences_update_recovery); |
| |
| if (!Utils.isABDevice()) { |
| abPerfMode.setVisibility(View.GONE); |
| } |
| |
| SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); |
| autoCheckInterval.setSelection(Utils.getUpdateCheckSetting(this)); |
| autoDelete.setChecked(prefs.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false)); |
| dataWarning.setChecked(prefs.getBoolean(Constants.PREF_MOBILE_DATA_WARNING, true)); |
| abPerfMode.setChecked(prefs.getBoolean(Constants.PREF_AB_PERF_MODE, false)); |
| |
| if (getResources().getBoolean(R.bool.config_hideRecoveryUpdate)) { |
| // Hide the update feature if explicitly requested. |
| // Might be the case of A-only devices using prebuilt vendor images. |
| updateRecovery.setVisibility(View.GONE); |
| } else if (Utils.isRecoveryUpdateExecPresent()) { |
| updateRecovery.setChecked( |
| SystemProperties.getBoolean(Constants.UPDATE_RECOVERY_PROPERTY, false)); |
| } else { |
| // There is no recovery updater script in the device, so the feature is considered |
| // forcefully enabled, just to avoid users to be confused and complain that |
| // recovery gets overwritten. That's the case of A/B and recovery-in-boot devices. |
| updateRecovery.setChecked(true); |
| updateRecovery.setOnTouchListener(new View.OnTouchListener() { |
| private Toast forcedUpdateToast = null; |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| if (forcedUpdateToast != null) { |
| forcedUpdateToast.cancel(); |
| } |
| forcedUpdateToast = Toast.makeText(getApplicationContext(), |
| getString(R.string.toast_forced_update_recovery), Toast.LENGTH_SHORT); |
| forcedUpdateToast.show(); |
| return true; |
| } |
| }); |
| } |
| |
| new AlertDialog.Builder(this) |
| .setTitle(R.string.menu_preferences) |
| .setView(view) |
| .setOnDismissListener(dialogInterface -> { |
| prefs.edit() |
| .putInt(Constants.PREF_AUTO_UPDATES_CHECK_INTERVAL, |
| autoCheckInterval.getSelectedItemPosition()) |
| .putBoolean(Constants.PREF_AUTO_DELETE_UPDATES, autoDelete.isChecked()) |
| .putBoolean(Constants.PREF_MOBILE_DATA_WARNING, dataWarning.isChecked()) |
| .putBoolean(Constants.PREF_AB_PERF_MODE, abPerfMode.isChecked()) |
| .apply(); |
| |
| if (Utils.isUpdateCheckEnabled(this)) { |
| UpdatesCheckReceiver.scheduleRepeatingUpdatesCheck(this); |
| } else { |
| UpdatesCheckReceiver.cancelRepeatingUpdatesCheck(this); |
| UpdatesCheckReceiver.cancelUpdatesCheck(this); |
| } |
| |
| if (Utils.isABDevice()) { |
| boolean enableABPerfMode = abPerfMode.isChecked(); |
| mUpdaterService.getUpdaterController().setPerformanceMode(enableABPerfMode); |
| } |
| if (Utils.isRecoveryUpdateExecPresent()) { |
| boolean enableRecoveryUpdate = updateRecovery.isChecked(); |
| SystemProperties.set(Constants.UPDATE_RECOVERY_PROPERTY, |
| String.valueOf(enableRecoveryUpdate)); |
| } |
| }) |
| .show(); |
| } |
| } |