SyncedFoldersActivity.java 38 KB

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