SyncedFoldersActivity.java 37 KB

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