SyncedFoldersActivity.java 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Andy Scherzinger
  5. * Copyright (C) 2016 Andy Scherzinger
  6. * Copyright (C) 2016 Nextcloud
  7. * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
  8. *
  9. * This program is free software; you can redistribute it and/or
  10. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  11. * License as published by the Free Software Foundation; either
  12. * version 3 of the License, or any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public
  20. * License along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. */
  22. package com.owncloud.android.ui.activity;
  23. import android.annotation.SuppressLint;
  24. import android.app.Activity;
  25. import android.app.NotificationManager;
  26. import android.content.Context;
  27. import android.content.Intent;
  28. import android.content.pm.PackageManager;
  29. import android.graphics.drawable.Drawable;
  30. import android.net.Uri;
  31. import android.os.Build;
  32. import android.os.Bundle;
  33. import android.os.PowerManager;
  34. import android.text.TextUtils;
  35. import android.util.Log;
  36. import android.view.Menu;
  37. import android.view.MenuInflater;
  38. import android.view.MenuItem;
  39. import android.view.View;
  40. import com.nextcloud.client.account.User;
  41. import com.nextcloud.client.core.Clock;
  42. import com.nextcloud.client.device.PowerManagementService;
  43. import com.nextcloud.client.di.Injectable;
  44. import com.nextcloud.client.jobs.BackgroundJobManager;
  45. import com.nextcloud.client.jobs.MediaFoldersDetectionWork;
  46. import com.nextcloud.client.jobs.NotificationWork;
  47. import com.nextcloud.client.preferences.AppPreferences;
  48. import com.nextcloud.java.util.Optional;
  49. import com.owncloud.android.BuildConfig;
  50. import com.owncloud.android.MainApp;
  51. import com.owncloud.android.R;
  52. import com.owncloud.android.databinding.SyncedFoldersLayoutBinding;
  53. import com.owncloud.android.datamodel.ArbitraryDataProvider;
  54. import com.owncloud.android.datamodel.MediaFolder;
  55. import com.owncloud.android.datamodel.MediaFolderType;
  56. import com.owncloud.android.datamodel.MediaProvider;
  57. import com.owncloud.android.datamodel.OCFile;
  58. import com.owncloud.android.datamodel.SyncedFolder;
  59. import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
  60. import com.owncloud.android.datamodel.SyncedFolderProvider;
  61. import com.owncloud.android.files.services.FileUploader;
  62. import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
  63. import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
  64. import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
  65. import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
  66. import com.owncloud.android.utils.DisplayUtils;
  67. import com.owncloud.android.utils.PermissionUtil;
  68. import com.owncloud.android.utils.SyncedFolderUtils;
  69. import com.owncloud.android.utils.ThemeUtils;
  70. import java.io.File;
  71. import java.util.ArrayList;
  72. import java.util.Collections;
  73. import java.util.HashMap;
  74. import java.util.List;
  75. import java.util.Locale;
  76. import java.util.Map;
  77. import javax.inject.Inject;
  78. import androidx.annotation.NonNull;
  79. import androidx.appcompat.app.AlertDialog;
  80. import androidx.core.content.res.ResourcesCompat;
  81. import androidx.drawerlayout.widget.DrawerLayout;
  82. import androidx.fragment.app.FragmentManager;
  83. import androidx.fragment.app.FragmentTransaction;
  84. import androidx.lifecycle.Lifecycle;
  85. import androidx.recyclerview.widget.GridLayoutManager;
  86. import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
  87. import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID;
  88. /**
  89. * Activity displaying all auto-synced folders and/or instant upload media folders.
  90. */
  91. public class SyncedFoldersActivity extends FileActivity implements SyncedFolderAdapter.ClickListener,
  92. SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener, Injectable {
  93. private static final String[] PRIORITIZED_FOLDERS = new String[]{"Camera", "Screenshots"};
  94. private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
  95. private static final String TAG = SyncedFoldersActivity.class.getSimpleName();
  96. private SyncedFoldersLayoutBinding binding;
  97. private SyncedFolderAdapter adapter;
  98. private SyncedFolderProvider syncedFolderProvider;
  99. private SyncedFolderPreferencesDialogFragment syncedFolderPreferencesDialogFragment;
  100. private String path;
  101. private int type;
  102. @Inject AppPreferences preferences;
  103. @Inject PowerManagementService powerManagementService;
  104. @Inject Clock clock;
  105. @Inject BackgroundJobManager backgroundJobManager;
  106. @Override
  107. protected void onCreate(Bundle savedInstanceState) {
  108. super.onCreate(savedInstanceState);
  109. binding = SyncedFoldersLayoutBinding.inflate(getLayoutInflater());
  110. setContentView(binding.getRoot());
  111. if (getIntent() != null && getIntent().getExtras() != null) {
  112. final String accountName = getIntent().getExtras().getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT);
  113. Optional<User> optionalUser = getUser();
  114. if (optionalUser.isPresent() && accountName != null) {
  115. User user = optionalUser.get();
  116. if (!accountName.equalsIgnoreCase(user.getAccountName())) {
  117. accountManager.setCurrentOwnCloudAccount(accountName);
  118. setUser(getUserAccountManager().getUser());
  119. }
  120. }
  121. path = getIntent().getStringExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_PATH);
  122. type = getIntent().getIntExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_TYPE, -1);
  123. // Cancel notification
  124. int notificationId = getIntent().getIntExtra(MediaFoldersDetectionWork.NOTIFICATION_ID, 0);
  125. NotificationManager notificationManager =
  126. (NotificationManager) getSystemService(Activity.NOTIFICATION_SERVICE);
  127. notificationManager.cancel(notificationId);
  128. }
  129. // setup toolbar
  130. setupToolbar();
  131. updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_synced_folders));
  132. setupDrawer();
  133. setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
  134. if (getSupportActionBar() != null) {
  135. getSupportActionBar().setDisplayHomeAsUpEnabled(true);
  136. }
  137. if (mDrawerToggle != null) {
  138. mDrawerToggle.setDrawerIndicatorEnabled(false);
  139. }
  140. // TODO: The content loading should be done asynchronously
  141. setupContent();
  142. if (ThemeUtils.themingEnabled(this)) {
  143. setTheme(R.style.FallbackThemingTheme);
  144. }
  145. binding.emptyList.emptyListViewAction.setOnClickListener(v -> showHiddenItems());
  146. }
  147. @Override
  148. public boolean onCreateOptionsMenu(Menu menu) {
  149. MenuInflater inflater = getMenuInflater();
  150. inflater.inflate(R.menu.activity_synced_folders, menu);
  151. if (powerManagementService.isPowerSavingExclusionAvailable()) {
  152. MenuItem item = menu.findItem(R.id.action_disable_power_save_check);
  153. item.setVisible(true);
  154. item.setChecked(preferences.isPowerCheckDisabled());
  155. item.setOnMenuItemClickListener(this::onDisablePowerSaveCheckClicked);
  156. }
  157. return true;
  158. }
  159. private boolean onDisablePowerSaveCheckClicked(MenuItem powerCheck) {
  160. if (!powerCheck.isChecked()) {
  161. showPowerCheckDialog();
  162. }
  163. preferences.setPowerCheckDisabled(!powerCheck.isChecked());
  164. powerCheck.setChecked(!powerCheck.isChecked());
  165. return true;
  166. }
  167. private void showPowerCheckDialog() {
  168. AlertDialog alertDialog = new AlertDialog.Builder(this)
  169. .setView(findViewById(R.id.root_layout))
  170. .setPositiveButton(R.string.common_ok, (dialog, which) -> dialog.dismiss())
  171. .setTitle(R.string.autoupload_disable_power_save_check)
  172. .setMessage(getString(R.string.power_save_check_dialog_message))
  173. .show();
  174. alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ThemeUtils.primaryAccentColor(this));
  175. }
  176. /**
  177. * sets up the UI elements and loads all media/synced folders.
  178. */
  179. private void setupContent() {
  180. final int gridWidth = getResources().getInteger(R.integer.media_grid_width);
  181. boolean lightVersion = getResources().getBoolean(R.bool.syncedFolder_light);
  182. adapter = new SyncedFolderAdapter(this, clock, gridWidth, this, lightVersion);
  183. syncedFolderProvider = new SyncedFolderProvider(getContentResolver(), preferences, clock);
  184. binding.emptyList.emptyListIcon.setImageResource(R.drawable.nav_synced_folders);
  185. ThemeUtils.colorPrimaryButton(binding.emptyList.emptyListViewAction, this);
  186. final GridLayoutManager lm = new GridLayoutManager(this, gridWidth);
  187. adapter.setLayoutManager(lm);
  188. int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
  189. binding.list.addItemDecoration(new MediaGridItemDecoration(spacing));
  190. binding.list.setLayoutManager(lm);
  191. binding.list.setAdapter(adapter);
  192. load(gridWidth * 2, false);
  193. }
  194. public void showHiddenItems() {
  195. if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() > adapter.getSectionCount()) {
  196. adapter.toggleHiddenItemsVisibility();
  197. binding.emptyList.emptyListView.setVisibility(View.GONE);
  198. binding.list.setVisibility(View.VISIBLE);
  199. }
  200. }
  201. /**
  202. * loads all media/synced folders, adds them to the recycler view adapter and shows the list.
  203. *
  204. * @param perFolderMediaItemLimit the amount of media items to be loaded/shown per media folder
  205. */
  206. private void load(final int perFolderMediaItemLimit, boolean force) {
  207. if (adapter.getItemCount() > 0 && !force) {
  208. return;
  209. }
  210. showLoadingContent();
  211. final List<MediaFolder> mediaFolders = MediaProvider.getImageFolders(getContentResolver(),
  212. perFolderMediaItemLimit, this, false);
  213. mediaFolders.addAll(MediaProvider.getVideoFolders(getContentResolver(), perFolderMediaItemLimit,
  214. this, false));
  215. List<SyncedFolder> syncedFolderArrayList = syncedFolderProvider.getSyncedFolders();
  216. List<SyncedFolder> currentAccountSyncedFoldersList = new ArrayList<>();
  217. User user = getUserAccountManager().getUser();
  218. for (SyncedFolder syncedFolder : syncedFolderArrayList) {
  219. if (syncedFolder.getAccount().equals(user.getAccountName())) {
  220. // delete non-existing & disabled synced folders
  221. if (!new File(syncedFolder.getLocalPath()).exists() && !syncedFolder.isEnabled()) {
  222. syncedFolderProvider.deleteSyncedFolder(syncedFolder.getId());
  223. } else {
  224. currentAccountSyncedFoldersList.add(syncedFolder);
  225. }
  226. }
  227. }
  228. List<SyncedFolderDisplayItem> syncFolderItems = sortSyncedFolderItems(
  229. mergeFolderData(currentAccountSyncedFoldersList, mediaFolders));
  230. adapter.setSyncFolderItems(syncFolderItems);
  231. adapter.notifyDataSetChanged();
  232. showList();
  233. if (!TextUtils.isEmpty(path)) {
  234. int section = adapter.getSectionByLocalPathAndType(path, type);
  235. if (section >= 0) {
  236. onSyncFolderSettingsClick(section, adapter.get(section));
  237. }
  238. }
  239. }
  240. /**
  241. * Sorts list of {@link SyncedFolderDisplayItem}s.
  242. *
  243. * @param syncFolderItemList list of items to be sorted
  244. * @return sorted list of items
  245. */
  246. public static List<SyncedFolderDisplayItem> sortSyncedFolderItems(List<SyncedFolderDisplayItem>
  247. syncFolderItemList) {
  248. Collections.sort(syncFolderItemList, (f1, f2) -> {
  249. if (f1 == null && f2 == null) {
  250. return 0;
  251. } else if (f1 == null) {
  252. return -1;
  253. } else if (f2 == null) {
  254. return 1;
  255. } else if (f1.isEnabled() && f2.isEnabled()) {
  256. if (f1.getFolderName() == null) {
  257. return -1;
  258. }
  259. if (f2.getFolderName() == null) {
  260. return 1;
  261. }
  262. return f1.getFolderName().toLowerCase(Locale.getDefault()).compareTo(
  263. f2.getFolderName().toLowerCase(Locale.getDefault()));
  264. } else if (f1.getFolderName() == null && f2.getFolderName() == null) {
  265. return 0;
  266. } else if (f1.isEnabled()) {
  267. return -1;
  268. } else if (f2.isEnabled()) {
  269. return 1;
  270. } else if (f1.getFolderName() == null) {
  271. return -1;
  272. } else if (f2.getFolderName() == null) {
  273. return 1;
  274. }
  275. for (String folder : PRIORITIZED_FOLDERS) {
  276. if (folder.equals(f1.getFolderName()) && folder.equals(f2.getFolderName())) {
  277. return 0;
  278. } else if (folder.equals(f1.getFolderName())) {
  279. return -1;
  280. } else if (folder.equals(f2.getFolderName())) {
  281. return 1;
  282. }
  283. }
  284. return f1.getFolderName().toLowerCase(Locale.getDefault()).compareTo(
  285. f2.getFolderName().toLowerCase(Locale.getDefault()));
  286. });
  287. return syncFolderItemList;
  288. }
  289. /**
  290. * merges two lists of {@link SyncedFolder} and {@link MediaFolder} items into one of SyncedFolderItems.
  291. *
  292. * @param syncedFolders the synced folders
  293. * @param mediaFolders the media folders
  294. * @return the merged list of SyncedFolderItems
  295. */
  296. @NonNull
  297. private List<SyncedFolderDisplayItem> mergeFolderData(List<SyncedFolder> syncedFolders,
  298. @NonNull List<MediaFolder> mediaFolders) {
  299. Map<String, SyncedFolder> syncedFoldersMap = createSyncedFoldersMap(syncedFolders);
  300. List<SyncedFolderDisplayItem> result = new ArrayList<>();
  301. for (MediaFolder mediaFolder : mediaFolders) {
  302. if (syncedFoldersMap.containsKey(mediaFolder.absolutePath + "-" + mediaFolder.type)) {
  303. SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath + "-" + mediaFolder.type);
  304. syncedFoldersMap.remove(mediaFolder.absolutePath + "-" + mediaFolder.type);
  305. if (syncedFolder != null && SyncedFolderUtils.isQualifyingMediaFolder(syncedFolder)) {
  306. if (MediaFolderType.CUSTOM == syncedFolder.getType()) {
  307. result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
  308. } else {
  309. result.add(createSyncedFolder(syncedFolder, mediaFolder));
  310. }
  311. }
  312. } else {
  313. if (SyncedFolderUtils.isQualifyingMediaFolder(mediaFolder)) {
  314. result.add(createSyncedFolderFromMediaFolder(mediaFolder));
  315. }
  316. }
  317. }
  318. for (SyncedFolder syncedFolder : syncedFoldersMap.values()) {
  319. result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
  320. }
  321. return result;
  322. }
  323. @NonNull
  324. private SyncedFolderDisplayItem createSyncedFolderWithoutMediaFolder(@NonNull SyncedFolder syncedFolder) {
  325. File localFolder = new File(syncedFolder.getLocalPath());
  326. File[] files = SyncedFolderUtils.getFileList(localFolder);
  327. List<String> filePaths = getDisplayFilePathList(files);
  328. return new SyncedFolderDisplayItem(
  329. syncedFolder.getId(),
  330. syncedFolder.getLocalPath(),
  331. syncedFolder.getRemotePath(),
  332. syncedFolder.isWifiOnly(),
  333. syncedFolder.isChargingOnly(),
  334. syncedFolder.isExisting(),
  335. syncedFolder.isSubfolderByDate(),
  336. syncedFolder.getAccount(),
  337. syncedFolder.getUploadAction(),
  338. syncedFolder.getNameCollisionPolicyInt(),
  339. syncedFolder.isEnabled(),
  340. clock.getCurrentTime(),
  341. filePaths,
  342. localFolder.getName(),
  343. files.length,
  344. syncedFolder.getType(),
  345. syncedFolder.isHidden());
  346. }
  347. /**
  348. * creates a SyncedFolderDisplayItem merging a {@link SyncedFolder} and a {@link MediaFolder} object instance.
  349. *
  350. * @param syncedFolder the synced folder object
  351. * @param mediaFolder the media folder object
  352. * @return the created SyncedFolderDisplayItem
  353. */
  354. @NonNull
  355. private SyncedFolderDisplayItem createSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull MediaFolder mediaFolder) {
  356. return new SyncedFolderDisplayItem(
  357. syncedFolder.getId(),
  358. syncedFolder.getLocalPath(),
  359. syncedFolder.getRemotePath(),
  360. syncedFolder.isWifiOnly(),
  361. syncedFolder.isChargingOnly(),
  362. syncedFolder.isExisting(),
  363. syncedFolder.isSubfolderByDate(),
  364. syncedFolder.getAccount(),
  365. syncedFolder.getUploadAction(),
  366. syncedFolder.getNameCollisionPolicyInt(),
  367. syncedFolder.isEnabled(),
  368. clock.getCurrentTime(),
  369. mediaFolder.filePaths,
  370. mediaFolder.folderName,
  371. mediaFolder.numberOfFiles,
  372. mediaFolder.type,
  373. syncedFolder.isHidden());
  374. }
  375. /**
  376. * creates a {@link SyncedFolderDisplayItem} based on a {@link MediaFolder} object instance.
  377. *
  378. * @param mediaFolder the media folder object
  379. * @return the created SyncedFolderDisplayItem
  380. */
  381. @NonNull
  382. private SyncedFolderDisplayItem createSyncedFolderFromMediaFolder(@NonNull MediaFolder mediaFolder) {
  383. return new SyncedFolderDisplayItem(
  384. UNPERSISTED_ID,
  385. mediaFolder.absolutePath,
  386. getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName,
  387. true,
  388. false,
  389. true,
  390. false,
  391. getAccount().name,
  392. FileUploader.LOCAL_BEHAVIOUR_FORGET,
  393. FileUploader.NameCollisionPolicy.ASK_USER.serialize(),
  394. false,
  395. clock.getCurrentTime(),
  396. mediaFolder.filePaths,
  397. mediaFolder.folderName,
  398. mediaFolder.numberOfFiles,
  399. mediaFolder.type,
  400. false);
  401. }
  402. private List<String> getDisplayFilePathList(File... files) {
  403. List<String> filePaths = null;
  404. if (files != null && files.length > 0) {
  405. filePaths = new ArrayList<>();
  406. for (int i = 0; i < 7 && i < files.length; i++) {
  407. filePaths.add(files[i].getAbsolutePath());
  408. }
  409. }
  410. return filePaths;
  411. }
  412. /**
  413. * creates a lookup map for a list of given {@link SyncedFolder}s with their local path as the key.
  414. *
  415. * @param syncFolders list of {@link SyncedFolder}s
  416. * @return the lookup map for {@link SyncedFolder}s
  417. */
  418. @NonNull
  419. private Map<String, SyncedFolder> createSyncedFoldersMap(List<SyncedFolder> syncFolders) {
  420. Map<String, SyncedFolder> result = new HashMap<>();
  421. if (syncFolders != null) {
  422. for (SyncedFolder syncFolder : syncFolders) {
  423. result.put(syncFolder.getLocalPath() + "-" + syncFolder.getType(), syncFolder);
  424. }
  425. }
  426. return result;
  427. }
  428. /**
  429. * show recycler view list or the empty message info (in case list is empty).
  430. */
  431. private void showList() {
  432. binding.list.setVisibility(View.VISIBLE);
  433. binding.loadingContent.setVisibility(View.GONE);
  434. checkAndShowEmptyListContent();
  435. }
  436. private void checkAndShowEmptyListContent() {
  437. if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() > adapter.getSectionCount()) {
  438. binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
  439. int hiddenFoldersCount = adapter.getHiddenFolderCount();
  440. showEmptyContent(getString(R.string.drawer_synced_folders),
  441. getResources().getQuantityString(R.plurals.synced_folders_show_hidden_folders,
  442. hiddenFoldersCount,
  443. hiddenFoldersCount),
  444. getResources().getQuantityString(R.plurals.synced_folders_show_hidden_folders,
  445. hiddenFoldersCount,
  446. hiddenFoldersCount));
  447. } else if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() == 0) {
  448. binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
  449. showEmptyContent(getString(R.string.drawer_synced_folders),
  450. getString(R.string.synced_folders_no_results));
  451. } else {
  452. binding.emptyList.emptyListView.setVisibility(View.GONE);
  453. }
  454. }
  455. @Override
  456. public boolean onOptionsItemSelected(MenuItem item) {
  457. boolean result = true;
  458. int itemId = item.getItemId();
  459. if (itemId == android.R.id.home) {
  460. finish();
  461. } else if (itemId == R.id.action_create_custom_folder) {
  462. Log.d(TAG, "Show custom folder dialog");
  463. SyncedFolderDisplayItem emptyCustomFolder = new SyncedFolderDisplayItem(
  464. UNPERSISTED_ID,
  465. null,
  466. null,
  467. true,
  468. false,
  469. true,
  470. false,
  471. getAccount().name,
  472. FileUploader.LOCAL_BEHAVIOUR_FORGET,
  473. FileUploader.NameCollisionPolicy.ASK_USER.serialize(),
  474. false,
  475. clock.getCurrentTime(),
  476. null,
  477. MediaFolderType.CUSTOM,
  478. false);
  479. onSyncFolderSettingsClick(0, emptyCustomFolder);
  480. result = super.onOptionsItemSelected(item);
  481. } else {
  482. result = super.onOptionsItemSelected(item);
  483. }
  484. return result;
  485. }
  486. @Override
  487. public void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
  488. if (syncedFolderDisplayItem.getId() > UNPERSISTED_ID) {
  489. syncedFolderProvider.updateSyncedFolderEnabled(syncedFolderDisplayItem.getId(),
  490. syncedFolderDisplayItem.isEnabled());
  491. } else {
  492. long storedId = syncedFolderProvider.storeSyncedFolder(syncedFolderDisplayItem);
  493. if (storedId != -1) {
  494. syncedFolderDisplayItem.setId(storedId);
  495. }
  496. }
  497. if (syncedFolderDisplayItem.isEnabled()) {
  498. backgroundJobManager.startImmediateFilesSyncJob(false, false);
  499. showBatteryOptimizationInfo();
  500. }
  501. }
  502. @Override
  503. public void onSyncFolderSettingsClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
  504. FragmentManager fm = getSupportFragmentManager();
  505. FragmentTransaction ft = fm.beginTransaction();
  506. ft.addToBackStack(null);
  507. syncedFolderPreferencesDialogFragment = SyncedFolderPreferencesDialogFragment.newInstance(
  508. syncedFolderDisplayItem, section);
  509. syncedFolderPreferencesDialogFragment.show(ft, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG);
  510. }
  511. @Override
  512. public void onVisibilityToggleClick(int section, SyncedFolderDisplayItem syncedFolder) {
  513. syncedFolder.setHidden(!syncedFolder.isHidden());
  514. saveOrUpdateSyncedFolder(syncedFolder);
  515. adapter.setSyncFolderItem(section, syncedFolder);
  516. checkAndShowEmptyListContent();
  517. }
  518. private void showEmptyContent(String headline, String message, String action) {
  519. showEmptyContent(headline, message);
  520. binding.emptyList.emptyListViewAction.setText(action);
  521. binding.emptyList.emptyListViewAction.setVisibility(View.VISIBLE);
  522. binding.emptyList.emptyListViewText.setVisibility(View.GONE);
  523. }
  524. private void showLoadingContent() {
  525. binding.loadingContent.setVisibility(View.VISIBLE);
  526. binding.emptyList.emptyListViewAction.setVisibility(View.GONE);
  527. }
  528. private void showEmptyContent(String headline, String message) {
  529. binding.emptyList.emptyListViewAction.setVisibility(View.GONE);
  530. binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
  531. binding.list.setVisibility(View.GONE);
  532. binding.loadingContent.setVisibility(View.GONE);
  533. binding.emptyList.emptyListViewHeadline.setText(headline);
  534. binding.emptyList.emptyListViewText.setText(message);
  535. binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE);
  536. binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE);
  537. }
  538. @Override
  539. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  540. if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_REMOTE_FOLDER
  541. && resultCode == RESULT_OK && syncedFolderPreferencesDialogFragment != null) {
  542. OCFile chosenFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
  543. syncedFolderPreferencesDialogFragment.setRemoteFolderSummary(chosenFolder.getRemotePath());
  544. }
  545. if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER
  546. && resultCode == RESULT_OK && syncedFolderPreferencesDialogFragment != null) {
  547. String localPath = data.getStringExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES);
  548. syncedFolderPreferencesDialogFragment.setLocalFolderSummary(localPath);
  549. } else {
  550. super.onActivityResult(requestCode, resultCode, data);
  551. }
  552. }
  553. @Override
  554. public void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
  555. // custom folders newly created aren't in the list already,
  556. // so triggering a refresh
  557. if (MediaFolderType.CUSTOM == syncedFolder.getType() && syncedFolder.getId() == UNPERSISTED_ID) {
  558. SyncedFolderDisplayItem newCustomFolder = new SyncedFolderDisplayItem(
  559. SyncedFolder.UNPERSISTED_ID,
  560. syncedFolder.getLocalPath(),
  561. syncedFolder.getRemotePath(),
  562. syncedFolder.isWifiOnly(),
  563. syncedFolder.isChargingOnly(),
  564. syncedFolder.isExisting(),
  565. syncedFolder.isSubfolderByDate(),
  566. syncedFolder.getAccount(),
  567. syncedFolder.getUploadAction(),
  568. syncedFolder.getNameCollisionPolicy().serialize(),
  569. syncedFolder.isEnabled(),
  570. clock.getCurrentTime(),
  571. new File(syncedFolder.getLocalPath()).getName(),
  572. syncedFolder.getType(),
  573. syncedFolder.isHidden());
  574. saveOrUpdateSyncedFolder(newCustomFolder);
  575. adapter.addSyncFolderItem(newCustomFolder);
  576. } else {
  577. SyncedFolderDisplayItem item = adapter.get(syncedFolder.getSection());
  578. updateSyncedFolderItem(item,
  579. syncedFolder.getId(),
  580. syncedFolder.getLocalPath(),
  581. syncedFolder.getRemotePath(),
  582. syncedFolder.isWifiOnly(),
  583. syncedFolder.isChargingOnly(),
  584. syncedFolder.isExisting(),
  585. syncedFolder.isSubfolderByDate(),
  586. syncedFolder.getUploadAction(),
  587. syncedFolder.getNameCollisionPolicy().serialize(),
  588. syncedFolder.isEnabled());
  589. saveOrUpdateSyncedFolder(item);
  590. // TODO test if notifyItemChanged is sufficient (should improve performance)
  591. adapter.notifyDataSetChanged();
  592. }
  593. syncedFolderPreferencesDialogFragment = null;
  594. if (syncedFolder.isEnabled()) {
  595. showBatteryOptimizationInfo();
  596. }
  597. }
  598. private void saveOrUpdateSyncedFolder(SyncedFolderDisplayItem item) {
  599. if (item.getId() == UNPERSISTED_ID) {
  600. // newly set up folder sync config
  601. storeSyncedFolder(item);
  602. } else {
  603. // existing synced folder setup to be updated
  604. syncedFolderProvider.updateSyncFolder(item);
  605. if (item.isEnabled()) {
  606. backgroundJobManager.startImmediateFilesSyncJob(false, false);
  607. } else {
  608. String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
  609. ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().
  610. getContentResolver());
  611. arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
  612. }
  613. }
  614. }
  615. private void storeSyncedFolder(SyncedFolderDisplayItem item) {
  616. ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().
  617. getContentResolver());
  618. long storedId = syncedFolderProvider.storeSyncedFolder(item);
  619. if (storedId != -1) {
  620. item.setId(storedId);
  621. if (item.isEnabled()) {
  622. backgroundJobManager.startImmediateFilesSyncJob(false, false);
  623. } else {
  624. String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
  625. arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
  626. }
  627. }
  628. }
  629. @Override
  630. public void onCancelSyncedFolderPreference() {
  631. syncedFolderPreferencesDialogFragment = null;
  632. }
  633. @Override
  634. public void onDeleteSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
  635. syncedFolderProvider.deleteSyncedFolder(syncedFolder.getId());
  636. adapter.removeItem(syncedFolder.getSection());
  637. }
  638. /**
  639. * update given synced folder with the given values.
  640. *
  641. * @param item the synced folder to be updated
  642. * @param localPath the local path
  643. * @param remotePath the remote path
  644. * @param wifiOnly upload on wifi only
  645. * @param chargingOnly upload on charging only
  646. * @param existing also upload existing
  647. * @param subfolderByDate created sub folders
  648. * @param uploadAction upload action
  649. * @param nameCollisionPolicy what to do on name collision
  650. * @param enabled is sync enabled
  651. */
  652. private void updateSyncedFolderItem(SyncedFolderDisplayItem item,
  653. long id,
  654. String localPath,
  655. String remotePath,
  656. boolean wifiOnly,
  657. boolean chargingOnly,
  658. boolean existing,
  659. boolean subfolderByDate,
  660. Integer uploadAction,
  661. Integer nameCollisionPolicy,
  662. boolean enabled) {
  663. item.setId(id);
  664. item.setLocalPath(localPath);
  665. item.setRemotePath(remotePath);
  666. item.setWifiOnly(wifiOnly);
  667. item.setChargingOnly(chargingOnly);
  668. item.setExisting(existing);
  669. item.setSubfolderByDate(subfolderByDate);
  670. item.setUploadAction(uploadAction);
  671. item.setNameCollisionPolicy(nameCollisionPolicy);
  672. item.setEnabled(enabled, clock.getCurrentTime());
  673. }
  674. @Override
  675. public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
  676. @NonNull int[] grantResults) {
  677. switch (requestCode) {
  678. case PermissionUtil.PERMISSIONS_WRITE_EXTERNAL_STORAGE: {
  679. // If request is cancelled, result arrays are empty.
  680. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  681. // permission was granted
  682. int gridWidth = getResources().getInteger(R.integer.media_grid_width);
  683. load(gridWidth * 2, true);
  684. } else {
  685. // permission denied --> do nothing
  686. return;
  687. }
  688. return;
  689. }
  690. default:
  691. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  692. }
  693. }
  694. @Override
  695. protected void onResume() {
  696. super.onResume();
  697. }
  698. private void showBatteryOptimizationInfo() {
  699. if (powerManagementService.isPowerSavingExclusionAvailable() || checkIfBatteryOptimizationEnabled()) {
  700. AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog)
  701. .setTitle(getString(R.string.battery_optimization_title))
  702. .setMessage(getString(R.string.battery_optimization_message))
  703. .setPositiveButton(getString(R.string.battery_optimization_disable), (dialog, which) -> {
  704. // show instant upload
  705. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  706. @SuppressLint("BatteryLife")
  707. Intent intent = new Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
  708. Uri.parse("package:" + BuildConfig.APPLICATION_ID));
  709. if (intent.resolveActivity(getPackageManager()) != null) {
  710. startActivity(intent);
  711. }
  712. } else {
  713. Intent powerUsageIntent = new Intent(Intent.ACTION_POWER_USAGE_SUMMARY);
  714. if (getPackageManager().resolveActivity(powerUsageIntent, 0) != null) {
  715. startActivity(powerUsageIntent);
  716. } else {
  717. dialog.dismiss();
  718. DisplayUtils.showSnackMessage(this, getString(R.string.battery_optimization_no_setting));
  719. }
  720. }
  721. })
  722. .setNegativeButton(getString(R.string.battery_optimization_close), (dialog, which) -> dialog.dismiss())
  723. .setIcon(R.drawable.ic_battery_alert);
  724. if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
  725. AlertDialog alertDialog = alertDialogBuilder.show();
  726. int color = ThemeUtils.primaryAccentColor(this);
  727. alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(color);
  728. alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(color);
  729. }
  730. }
  731. }
  732. /**
  733. * Check if battery optimization is enabled. If unknown, fallback to true.
  734. *
  735. * @return true if battery optimization is enabled
  736. */
  737. private boolean checkIfBatteryOptimizationEnabled() {
  738. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  739. PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
  740. if (powerManager == null) {
  741. return true;
  742. }
  743. return !powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID);
  744. } else {
  745. return true;
  746. }
  747. }
  748. }